From fb6ef7c5aa76964bb0daac5ea6f19cc895462a2f Mon Sep 17 00:00:00 2001 From: Sergey Polishchuk Date: Tue, 20 May 2025 14:43:13 +0300 Subject: [PATCH] Release 052025 (#100) * Update Python version and refactor CI/CD pipeline Upgraded Python version to 3.10.13. Refactored GitHub Actions workflows, renamed 'main.yml' to 'ci.yml', introduced new workflows for auto-formatter, and author assignment. Moreover, the 'requirements.txt' and 'setup.py' files have been removed as dependencies are managed by Poetry. (cherry picked from commit 5300951d152915c835d8e3ff541210bd08e323d9) * Refactor codebase to use pathlib and type annotations The codebase has been refactored to use the pathlib module for file handling operations, replacing the os module. Additionally, type annotations have been added throughout the codebase to improve readability and facilitate static type checking. These changes aim to enhance both the maintainability and reliability of the code. (cherry picked from commit e2ade205e1c2324b7b30911122542307dda0906d) * Update workflow configurations for Github Actions Modified the `.github/workflows/ci.yml` and `.github/workflows/auto-format.yml` files to streamline operations. Added a concurrency control which allows concurrent jobs to run in the workflow, and updated the names of the listed branches by removing unnecessary quotes. Also renamed `test-ubuntu` to `test` while improving its descriptor in the CI workflow. (cherry picked from commit f525391f2c12b8d3cd1560640bf78ff695563348) * Update Poetry install command in CI config Updated the poetry install command in the continuous integration configuration (".github/workflows/ci.yml"). Removed the other flags and just added the "--sync" option for the Poetry install command to enforce a correct and complete installation of dependencies. (cherry picked from commit 9f8ec792bb68a8574e06eb8fc8bfbdf622c9f9a5) * Update poetry install command in workflow files The poetry install command in both '.github/workflows/actions/prepare/action.yml' and '.github/workflows/ci.yml' workflow files has been updated. The new command now includes '--no-root' option for better control of the installation process and removed the '--only main,dev' options to ensure all necessary dependencies are installed. (cherry picked from commit 048b9cc490549bad7663c1d920f283154ab4ba45) * Refactor base_searcher and stage_test for improved exception handling and type annotations * Refactor base_searcher to improve path handling and enhance readability * Refactor base_searcher to simplify source path handling and improve clarity * Improve syntax error messages in eval tests * Refactor error messages in test cases for clarity and consistency * Update GitHub Actions workflow to use Python 3.12 and latest action versions * Add CI workflow for linting, type checking, and testing with multiple Python versions * Migrate project to Poetry for dependency management and update Python version requirements * Backend: Auto format * Update CI workflow to use arc-runners-small for Python versions 3.10, 3.11, and 3.12 * Enhance CI workflow to support Poetry installation on both Unix and Windows environments * Refactor graph type handling and update requirements for cross-platform compatibility * Add type hints to test files for improved clarity and type checking * Add type hints to test files for improved clarity and type checking * Add type hints to test file for improved clarity and type checking * Add type hints to test file for improved clarity and type checking * Add CheckResult import to test files for enhanced result checking * Add type hints to test files for improved clarity and type checking * Refactor CI workflow and add type hint for OutcomeError in stage_test.py * Refactor matplotlib_handler to use direct import for Axes and improve exception handling in stage_test * Backend: Auto format * Enhance error message normalization in ExpectedFailTest for improved feedback clarity * Backend: Auto format * Update CI workflow to use latest Ubuntu and Windows runners with multiple Python versions * Enhance error handling and platform-specific command execution in Python executor * Backend: Auto format * Enhance error handling and platform-specific command execution in Python executor * Enhance error handling and platform-specific command execution in Python executor * Backend: Auto format * Refactor python_executor to use is_windows utility for OS checks * Enhance error handling and Windows compatibility in failure handler and process wrapper * Enhance traceback formatting in failure handler and update CI workflow for Windows compatibility * Backend: Auto format * Enhance input handling in process wrapper to ensure newline termination and improve error handling * Backend: Auto format * Enhance CI workflow to include Python 3.11 and 3.12 for Windows compatibility * Enhance CI workflow to configure environment variables for Windows compatibility * Enhance CI workflow to uncomment Python versions for Ubuntu and macOS * Add GitHub Actions workflow for automatic code formatting with ruff * Enhance CI workflow to update wheel URLs and manage releases automatically * Refactor CI workflows to improve job names and streamline formatting steps * Update CI workflow to rename lint job and streamline ruff formatting steps * fix: * fix: * Update dependencies: bump psutil to 7.0.0, mypy to 1.13.0, pandas to 2.2.3, scipy to 1.15.3, and ruff to 0.7.3 * Backend: Auto format * Add .go-version and .node-version files; update action.yml to dynamically set Node.js and Go versions * Add support for Python 3.13 in CI workflow * Add Go version 1.21 to .go-version file * Fix: dynamically import PlottingTest and update __all__ list * Fix: dynamically import PlottingTest and update __all__ list * Backend: Auto format --------- Co-authored-by: meanmail Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .flake8 | 23 - .github/dependabot.yml | 17 + .github/workflows/actions/prepare/action.yml | 100 ++ .github/workflows/auto-author-assign.yml | 16 + .github/workflows/build-wheels.yml | 141 +++ .github/workflows/ci.yml | 111 ++ .github/workflows/main.yml | 2 + .go-version | 1 + .node-version | 1 + .poetry-version | 1 + .python-version | 1 + hstest/__init__.py | 38 +- hstest/check_result.py | 2 + hstest/common/__init__.py | 0 hstest/common/file_utils.py | 24 +- hstest/common/os_utils.py | 8 +- hstest/common/process_utils.py | 41 +- hstest/common/reflection_utils.py | 92 +- hstest/common/utils.py | 34 +- hstest/dynamic/__init__.py | 0 hstest/dynamic/dynamic_test.py | 34 +- hstest/dynamic/input/__init__.py | 0 hstest/dynamic/input/dynamic_input_func.py | 14 +- hstest/dynamic/input/dynamic_input_handler.py | 21 +- hstest/dynamic/input/dynamic_testing.py | 121 +- hstest/dynamic/input/input_handler.py | 15 +- hstest/dynamic/input/input_mock.py | 47 +- hstest/dynamic/output/__init__.py | 0 hstest/dynamic/output/colored_output.py | 2 + .../dynamic/output/infinite_loop_detector.py | 59 +- hstest/dynamic/output/output_handler.py | 35 +- hstest/dynamic/output/output_mock.py | 60 +- hstest/dynamic/security/__init__.py | 0 hstest/dynamic/security/exit_exception.py | 9 +- hstest/dynamic/security/exit_handler.py | 24 +- hstest/dynamic/security/thread_group.py | 20 +- hstest/dynamic/security/thread_handler.py | 24 +- hstest/dynamic/system_handler.py | 26 +- hstest/exception/__init__.py | 0 hstest/exception/failure_handler.py | 29 +- hstest/exception/outcomes.py | 18 +- hstest/exception/testing.py | 6 +- hstest/exceptions.py | 2 + hstest/outcomes/__init__.py | 0 hstest/outcomes/compilation_error_outcome.py | 12 +- hstest/outcomes/error_outcome.py | 32 +- hstest/outcomes/exception_outcome.py | 20 +- hstest/outcomes/outcome.py | 73 +- hstest/outcomes/unexpected_error_outcome.py | 11 +- hstest/outcomes/wrong_answer_outcome.py | 14 +- hstest/stage/__init__.py | 18 +- hstest/stage/django_test.py | 17 +- hstest/stage/flask_test.py | 28 +- hstest/stage/plotting_test.py | 19 +- hstest/stage/sql_test.py | 9 +- hstest/stage/stage_test.py | 119 +- hstest/stage_test.py | 4 +- hstest/test_case/__init__.py | 12 +- hstest/test_case/attach/__init__.py | 0 hstest/test_case/attach/django_settings.py | 6 +- hstest/test_case/attach/flask_settings.py | 6 +- hstest/test_case/check_result.py | 18 +- hstest/test_case/test_case.py | 106 +- hstest/testing/__init__.py | 0 hstest/testing/execution/__init__.py | 0 .../testing/execution/filtering/__init__.py | 0 .../execution/filtering/file_filter.py | 38 +- .../execution/filtering/main_filter.py | 31 +- .../testing/execution/main_module_executor.py | 41 +- hstest/testing/execution/process/__init__.py | 0 .../testing/execution/process/cpp_executor.py | 23 +- .../testing/execution/process/go_executor.py | 20 +- .../execution/process/javascript_executor.py | 6 +- .../execution/process/python_executor.py | 17 +- .../execution/process/shell_executor.py | 6 +- hstest/testing/execution/process_executor.py | 96 +- hstest/testing/execution/program_executor.py | 69 +- hstest/testing/execution/runnable/__init__.py | 0 .../runnable/python_runnable_file.py | 4 +- .../execution/runnable/runnable_file.py | 9 +- hstest/testing/execution/searcher/__init__.py | 0 .../execution/searcher/base_searcher.py | 158 +-- .../execution/searcher/cpp_searcher.py | 21 +- .../testing/execution/searcher/go_searcher.py | 26 +- .../execution/searcher/javascript_searcher.py | 15 +- .../execution/searcher/python_searcher.py | 41 +- .../execution/searcher/shell_searcher.py | 15 +- .../execution/searcher/sql_searcher.py | 13 +- hstest/testing/execution_options.py | 12 +- hstest/testing/plotting/__init__.py | 0 hstest/testing/plotting/drawing/__init__.py | 0 hstest/testing/plotting/drawing/drawing.py | 20 +- .../plotting/drawing/drawing_builder.py | 20 +- .../testing/plotting/drawing/drawing_data.py | 14 +- .../plotting/drawing/drawing_library.py | 3 + .../testing/plotting/drawing/drawing_type.py | 3 + .../plotting/drawing_data_normalizer.py | 18 +- hstest/testing/plotting/matplotlib_handler.py | 149 ++- hstest/testing/plotting/pandas_handler.py | 322 ++---- hstest/testing/plotting/seaborn_handler.py | 160 ++- hstest/testing/process_wrapper.py | 186 +-- hstest/testing/runner/__init__.py | 0 .../runner/async_dynamic_testing_runner.py | 29 +- .../runner/django_application_runner.py | 87 +- .../runner/flask_application_runner.py | 68 +- hstest/testing/runner/plot_testing_runner.py | 34 +- hstest/testing/runner/sql_runner.py | 35 +- hstest/testing/runner/test_runner.py | 15 +- hstest/testing/settings.py | 8 +- hstest/testing/state_machine.py | 38 +- hstest/testing/test_run.py | 52 +- hstest/testing/tested_program.py | 39 +- hstest/testing/unittest/__init__.py | 0 hstest/testing/unittest/expected_fail_test.py | 47 +- .../unittest/unexepected_error_test.py | 4 +- hstest/testing/unittest/user_error_test.py | 4 +- poetry.lock | 1000 +++++++++++++++++ pyproject.toml | 176 ++- requirements-dev.txt | 6 - requirements.txt | 9 +- setup.py | 26 - tests/__init__.py | 0 .../test.py | 4 +- .../test_dynamic_method_exception/test.py | 35 +- .../feedback_on_exception_test_1/test.py | 1 + .../lib/exception_in_user_code_2/test.py | 2 + .../lib/exception_in_user_code_3/test.py | 7 +- .../plot/bar/pandas/test_example/cleaning.py | 2 +- .../plot/test_seaborn/test_revert/main.py | 2 +- .../syntax_error/test_empty_eval/test.py | 7 +- .../test_error_using_eval/test.py | 7 +- .../test_error_using_eval_and_print/test.py | 10 +- .../go/coffee_machine/stage1_ce/tests.py | 7 +- .../go/coffee_machine/stage1_ex/tests.py | 5 +- .../go/coffee_machine/stage1_wa/tests.py | 5 +- .../go/coffee_machine/stage2/tests.py | 3 + .../go/coffee_machine/stage3/tests.py | 3 + .../go/coffee_machine/stage4/tests.py | 3 + .../go/coffee_machine/stage5/tests.py | 3 + .../javascript/coffee_machine/stage1/tests.py | 3 + .../coffee_machine/stage1_ce/tests.py | 5 +- .../coffee_machine/stage1_ex/tests.py | 5 +- .../coffee_machine/stage1_wa/tests.py | 5 +- .../javascript/coffee_machine/stage2/tests.py | 5 +- .../javascript/coffee_machine/stage3/tests.py | 3 + .../javascript/coffee_machine/stage4/tests.py | 3 + .../javascript/coffee_machine/stage5/tests.py | 3 + .../simple_chatty_bot/stage1/tests.py | 2 + .../simple_chatty_bot/stage2/tests.py | 3 + .../simple_chatty_bot/stage3/tests.py | 3 + .../simple_chatty_bot/stage4/tests.py | 3 + .../simple_chatty_bot/stage5/tests.py | 3 + .../python/coffee_machine/stage1/tests.py | 3 + .../python/coffee_machine/stage2/tests.py | 3 + .../python/coffee_machine/stage3/tests.py | 3 + .../python/coffee_machine/stage4/tests.py | 3 + .../python/coffee_machine/stage5/tests.py | 3 + .../shell/coffee_machine/stage1/tests.py | 3 + .../shell/coffee_machine/stage1_ex/tests.py | 3 + .../shell/coffee_machine/stage1_wa/tests.py | 3 + .../shell/coffee_machine/stage2/tests.py | 3 + tests/test_check_result.py | 12 +- tests/test_testcase.py | 70 +- tests/testing.py | 62 +- 164 files changed, 3539 insertions(+), 1760 deletions(-) delete mode 100644 .flake8 create mode 100644 .github/dependabot.yml create mode 100755 .github/workflows/actions/prepare/action.yml create mode 100644 .github/workflows/auto-author-assign.yml create mode 100755 .github/workflows/build-wheels.yml create mode 100755 .github/workflows/ci.yml mode change 100644 => 100755 .github/workflows/main.yml create mode 100644 .go-version create mode 100644 .node-version create mode 100755 .poetry-version create mode 100755 .python-version create mode 100644 hstest/common/__init__.py create mode 100644 hstest/dynamic/__init__.py create mode 100644 hstest/dynamic/input/__init__.py create mode 100644 hstest/dynamic/output/__init__.py create mode 100644 hstest/dynamic/security/__init__.py create mode 100644 hstest/exception/__init__.py create mode 100644 hstest/outcomes/__init__.py create mode 100644 hstest/test_case/attach/__init__.py create mode 100644 hstest/testing/__init__.py create mode 100644 hstest/testing/execution/__init__.py create mode 100644 hstest/testing/execution/filtering/__init__.py create mode 100644 hstest/testing/execution/process/__init__.py create mode 100644 hstest/testing/execution/runnable/__init__.py create mode 100644 hstest/testing/execution/searcher/__init__.py create mode 100644 hstest/testing/plotting/__init__.py create mode 100644 hstest/testing/plotting/drawing/__init__.py create mode 100644 hstest/testing/runner/__init__.py create mode 100644 hstest/testing/unittest/__init__.py create mode 100755 poetry.lock mode change 100644 => 100755 pyproject.toml delete mode 100644 requirements-dev.txt delete mode 100644 setup.py create mode 100644 tests/__init__.py 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/.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 100755 index 00000000..4ca83ab0 --- /dev/null +++ b/.github/workflows/actions/prepare/action.yml @@ -0,0 +1,100 @@ +name: 'Prepare environment' +description: 'Prepare environment' + +inputs: + python-version: + description: 'Python version to use' + required: true + +runs: + using: "composite" + steps: + - name: Set up Python paths (Unix) + if: runner.os != 'Windows' + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + shell: bash + + - name: Set up Python paths (Windows) + if: runner.os == 'Windows' + run: echo "$env:APPDATA\Python\Scripts" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + shell: pwsh + + - name: Get Poetry version + id: poetry-version + run: | + if [ -f .poetry-version ]; then + echo "version=$(head -n 1 .poetry-version)" >> $GITHUB_OUTPUT + else + echo "version=2.0.1" >> $GITHUB_OUTPUT + fi + shell: bash + + - name: Install Poetry (Unix) + if: runner.os != 'Windows' + run: pipx install poetry==${{ steps.poetry-version.outputs.version }} + shell: bash + + - name: Install Poetry (Windows) + if: runner.os == 'Windows' + run: pipx install poetry==${{ steps.poetry-version.outputs.version }} + shell: pwsh + + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + cache: 'poetry' + + - name: Clear Poetry cache (Unix) + if: runner.os != 'Windows' + run: poetry cache clear --all pypi + shell: bash + + - name: Clear Poetry cache (Windows) + if: runner.os == 'Windows' + run: poetry cache clear --all pypi + shell: pwsh + + - name: Install dependencies and package (Unix) + if: runner.os != 'Windows' + run: poetry install --no-interaction --no-ansi + shell: bash + + - name: Install dependencies and package (Windows) + if: runner.os == 'Windows' + run: poetry install --no-interaction --no-ansi + shell: pwsh + + # Setup Node.js for JavaScript tests + - name: Get Node.js version + id: node-version + run: | + if [ -f .node-version ]; then + echo "version=$(head -n 1 .node-version)" >> $GITHUB_OUTPUT + else + echo "version=20" >> $GITHUB_OUTPUT + fi + shell: bash + - uses: actions/setup-node@v4 + with: + node-version: ${{ steps.node-version.outputs.version }} + + # Install Node.js dependencies + - name: Install Node.js dependencies + run: npm install + shell: bash + + # Setup Go for Go language tests + - name: Get Go version + id: go-version + run: | + if [ -f .go-version ]; then + echo "version=$(head -n 1 .go-version)" >> $GITHUB_OUTPUT + else + echo "version=1.21" >> $GITHUB_OUTPUT + fi + shell: bash + + - uses: actions/setup-go@v5 + with: + go-version: ${{ steps.go-version.outputs.version }} + cache: 'false' diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml new file mode 100644 index 00000000..4694f847 --- /dev/null +++ b/.github/workflows/auto-author-assign.yml @@ -0,0 +1,16 @@ +name: Auto Author Assign + +on: + pull_request_target: + types: [ opened, reopened ] + +permissions: + pull-requests: write + +jobs: + assign-author: + runs-on: arc-runners-small + timeout-minutes: 30 + if: ${{ !github.event.pull_request.assignee }} + steps: + - uses: toshimaru/auto-author-assign@v2.1.0 diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml new file mode 100755 index 00000000..87f4c0b0 --- /dev/null +++ b/.github/workflows/build-wheels.yml @@ -0,0 +1,141 @@ +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: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: release + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true + + - name: Get tag version + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + shell: bash + + - name: Update wheel URLs in pyproject.toml + run: | + # Get the version without the 'v' prefix + VERSION="${{ steps.get_version.outputs.VERSION }}" + + # Update URLs in pyproject.toml + sed -i "s|/releases/download/v[0-9]\+\.[0-9]\+\.[0-9]\+/|/releases/download/${VERSION}/|g" pyproject.toml + + # Update poetry.lock + poetry lock --no-update + + # Commit changes + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git add pyproject.toml poetry.lock + git commit -m "Update wheel URLs to ${VERSION}" + + # Force update the release branch + git push origin release --force + + # Create/update tag to point to the new commit + git tag -f "${VERSION}" + git push origin "${VERSION}" --force + shell: bash + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: | + dist/* + pyproject.toml + body: | + Release ${{ steps.get_version.outputs.VERSION }} + + This release includes: + - Updated wheel builds for Python 3.10, 3.11, and 3.12 + - Updated pyproject.toml with correct wheel URLs + draft: false + prerelease: false + target_commitish: release + + - name: Merge release into master + run: | + git checkout master + git pull origin master + git merge release --no-ff -m "Merge release ${VERSION} into master" + git push origin master + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100755 index 00000000..0fdcccf8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,111 @@ +name: CI + +on: + push: + branches: + - master + - release + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + lint: + name: Lint with ruff + runs-on: arc-runners-small + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - uses: ./.github/workflows/actions/prepare + with: + python-version: "3.11" + - name: Check files using the ruff formatter + run: | + poetry run ruff check --fix --unsafe-fixes --preview --exit-zero . + poetry run ruff format . + shell: bash + - name: Commit changes + uses: EndBug/add-and-commit@v9 + with: + fetch: false + default_author: github_actions + message: 'Backend: Auto format' + add: '.' + + mypy: + name: Static Type Checking + needs: lint + runs-on: arc-runners-small + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/workflows/actions/prepare + with: + python-version: "3.12" + - name: Mypy + run: poetry run mypy . + shell: bash + + test: + name: Run unit test on ${{ matrix.os }} with Python ${{ matrix.python-version }} + needs: lint + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # Ubuntu + - os: ubuntu-latest + python-version: "3.10" + - os: ubuntu-latest + python-version: "3.11" + - os: ubuntu-latest + python-version: "3.12" + - os: ubuntu-latest + python-version: "3.13" + + # Windows + - os: windows-latest + python-version: "3.10" + - os: windows-latest + python-version: "3.11" + - os: windows-latest + python-version: "3.12" + - os: windows-latest + python-version: "3.13" + + # macOS (arm64) + - os: macos-14 + python-version: "3.10" + - os: macos-14 + python-version: "3.11" + - os: macos-14 + python-version: "3.12" + - os: macos-14 + python-version: "3.13" + steps: + - uses: actions/checkout@v4 + - uses: ./.github/workflows/actions/prepare + with: + python-version: ${{ matrix.python-version }} + # Set environment variables for Windows + - name: Configure Windows environment + if: runner.os == 'Windows' + run: | + echo "PYTHONIOENCODING=utf-8" >> $GITHUB_ENV + echo "PYTHONUTF8=1" >> $GITHUB_ENV + echo "PYTHONLEGACYWINDOWSSTDIO=0" >> $GITHUB_ENV + shell: bash + - name: Run Tests + run: poetry run python tests/testing.py + shell: bash + env: + PYTHONIOENCODING: utf-8 + PYTHONUTF8: 1 + PYTHONLEGACYWINDOWSSTDIO: 0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml old mode 100644 new mode 100755 index 8b94c925..869ce9e9 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,7 @@ on: [ push ] jobs: Lint: + if: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -24,6 +25,7 @@ jobs: - name: Lint run: flake8 test-ubuntu: + if: false runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/.go-version b/.go-version new file mode 100644 index 00000000..0f6abf48 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.21 \ No newline at end of file diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..2edeafb0 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +20 \ No newline at end of file diff --git a/.poetry-version b/.poetry-version new file mode 100755 index 00000000..10bf840e --- /dev/null +++ b/.poetry-version @@ -0,0 +1 @@ +2.0.1 \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100755 index 00000000..920121e4 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10.13 \ No newline at end of file diff --git a/hstest/__init__.py b/hstest/__init__.py index 86b7709e..8346bc80 100644 --- a/hstest/__init__.py +++ b/hstest/__init__.py @@ -1,22 +1,21 @@ -__all__ = [ - 'StageTest', - 'DjangoTest', - 'FlaskTest', - 'PlottingTest', - 'SQLTest', - - 'TestCase', - 'SimpleTestCase', - - 'CheckResult', - 'correct', - 'wrong', +from __future__ import annotations - 'WrongAnswer', - 'TestPassed', +from typing import Any - 'dynamic_test', - 'TestedProgram', +__all__ = [ + "CheckResult", + "DjangoTest", + "FlaskTest", + "SQLTest", + "SimpleTestCase", + "StageTest", + "TestCase", + "TestPassed", + "TestedProgram", + "WrongAnswer", + "correct", + "dynamic_test", + "wrong", ] from hstest.dynamic.dynamic_test import dynamic_test @@ -25,7 +24,12 @@ from hstest.test_case import CheckResult, correct, SimpleTestCase, TestCase, wrong from hstest.testing.tested_program import TestedProgram +# Define PlottingTest as Any before trying to import it +PlottingTest: Any + try: from hstest.stage import PlottingTest + + __all__.append("PlottingTest") except ImportError: PlottingTest = None 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..9da6ea14 100644 --- a/hstest/common/file_utils.py +++ b/hstest/common/file_utils.py @@ -1,42 +1,42 @@ +from __future__ import annotations + +import contextlib import os -from typing import Dict from hstest.exception.testing import FileDeletionError -def create_files(files: Dict[str, str]): +def create_files(files: dict[str, str]) -> None: for file, content in files.items(): - with open(file, 'w') as f: + with open(file, "w", encoding="utf-8") as f: f.write(content) -def delete_files(files: Dict[str, str]): - for file in files.keys(): +def delete_files(files: dict[str, str]) -> None: + for file in files: if os.path.isfile(file): try: os.remove(file) except PermissionError: - raise FileDeletionError() + raise FileDeletionError -def safe_delete(filename): +def safe_delete(filename) -> None: if os.path.exists(filename): - try: + with contextlib.suppress(BaseException): os.remove(filename) - except BaseException: - pass def walk_user_files(folder): curr_folder = os.path.abspath(folder) - test_folder = os.path.join(curr_folder, 'test') + test_folder = os.path.join(curr_folder, "test") for folder, dirs, files in os.walk(curr_folder): if folder.startswith(test_folder): continue if folder == curr_folder: - for file in 'test.py', 'tests.py': + for file in "test.py", "tests.py": if file in files: files.remove(file) diff --git a/hstest/common/os_utils.py b/hstest/common/os_utils.py index b3364de5..4c346919 100644 --- a/hstest/common/os_utils.py +++ b/hstest/common/os_utils.py @@ -1,13 +1,15 @@ +from __future__ import annotations + import platform def is_windows(): - return platform.system() == 'Windows' + return platform.system() == "Windows" def is_mac(): - return platform.system() == 'Darwin' + return platform.system() == "Darwin" def is_linux(): - return platform.system() == 'Linux' + return platform.system() == "Linux" diff --git a/hstest/common/process_utils.py b/hstest/common/process_utils.py index c8457c02..0c29949f 100644 --- a/hstest/common/process_utils.py +++ b/hstest/common/process_utils.py @@ -1,42 +1,40 @@ -import sys +from __future__ import annotations + import threading import weakref from concurrent.futures import ThreadPoolExecutor from concurrent.futures.thread import _worker +from typing import TYPE_CHECKING -from hstest.dynamic.security.thread_group import ThreadGroup +if TYPE_CHECKING: + from hstest.dynamic.security.thread_group import ThreadGroup class DaemonThreadPoolExecutor(ThreadPoolExecutor): - def __init__(self, max_workers: int = 1, name: str = '', group: ThreadGroup = None): + def __init__(self, max_workers: int = 1, name: str = "", group: ThreadGroup = None) -> None: super().__init__(max_workers=max_workers, thread_name_prefix=name) self.group = group # Adjusted method from the ThreadPoolExecutor class just to create threads as daemons - def _adjust_thread_count(self): - if sys.version_info >= (3, 8): - # if idle threads are available, don't spin new threads - if self._idle_semaphore.acquire(timeout=0): - return + def _adjust_thread_count(self) -> None: + if self._idle_semaphore.acquire(timeout=0): + return # When the executor gets lost, the weakref callback will wake up # the worker threads. - def weakref_cb(_, q=self._work_queue): + def weakref_cb(_, q=self._work_queue) -> None: q.put(None) num_threads = len(self._threads) if num_threads < self._max_workers: - thread_name = '%s_%d' % (self._thread_name_prefix or self, - num_threads) - - if sys.version_info >= (3, 7): - args = (weakref.ref(self, weakref_cb), - self._work_queue, - self._initializer, - self._initargs) - else: - args = (weakref.ref(self, weakref_cb), - self._work_queue) + thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads) + + args = ( + weakref.ref(self, weakref_cb), + self._work_queue, + self._initializer, + self._initargs, + ) t = threading.Thread(name=thread_name, target=_worker, args=args, group=self.group) t.daemon = True @@ -46,5 +44,6 @@ def weakref_cb(_, q=self._work_queue): def is_port_in_use(port): import socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - return s.connect_ex(('localhost', port)) == 0 + return s.connect_ex(("localhost", port)) == 0 diff --git a/hstest/common/reflection_utils.py b/hstest/common/reflection_utils.py index dc0f04ea..9816ebc6 100644 --- a/hstest/common/reflection_utils.py +++ b/hstest/common/reflection_utils.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import inspect +import itertools import os -from typing import List from hstest.exception.failure_handler import get_traceback_stack @@ -9,21 +11,21 @@ def is_tests(stage): package = inspect.getmodule(stage).__package__ file = inspect.getmodule(stage).__file__ return ( - package and package.startswith('tests.outcomes.') or - package and package.startswith('tests.projects.') or - file and f'{os.sep}hs-test-python{os.sep}tests{os.sep}outcomes{os.sep}' in file or - file and f'{os.sep}hs-test-python{os.sep}tests{os.sep}projects{os.sep}' in file or - file and f'{os.sep}hs-test-python{os.sep}tests{os.sep}sql{os.sep}' in file + (package and package.startswith("tests.outcomes.")) + or (package and package.startswith("tests.projects.")) + or (file and f"{os.sep}hs-test-python{os.sep}tests{os.sep}outcomes{os.sep}" in file) + or (file and f"{os.sep}hs-test-python{os.sep}tests{os.sep}projects{os.sep}" in file) + or (file and f"{os.sep}hs-test-python{os.sep}tests{os.sep}sql{os.sep}" in file) ) -def setup_cwd(stage): +def setup_cwd(stage) -> None: if stage.is_tests: test_file = inspect.getmodule(stage).__file__ test_folder = os.path.dirname(test_file) os.chdir(test_folder) - if os.path.basename(os.getcwd()) == 'test': + if os.path.basename(os.getcwd()) == "test": os.chdir(os.path.dirname(os.getcwd())) @@ -31,15 +33,15 @@ def get_stacktrace(ex: BaseException, hide_internals=False) -> str: traceback_stack = get_traceback_stack(ex) if not hide_internals: - return ''.join(traceback_stack) + return "".join(traceback_stack) if isinstance(ex, SyntaxError): - if ex.filename.startswith('<'): # "", or "" + if ex.filename.startswith("<"): # "", or "" user_dir = ex.filename else: user_dir = os.path.dirname(ex.filename) + os.sep else: - user_dir = '' + user_dir = "" user_traceback = [] for tr in traceback_stack[::-1][1:-1]: @@ -47,23 +49,21 @@ def get_stacktrace(ex: BaseException, hide_internals=False) -> str: break user_traceback += [tr] - user_traceback = [tr for tr in user_traceback - if f'{os.sep}hstest{os.sep}' not in tr] + user_traceback = [tr for tr in user_traceback if f"{os.sep}hstest{os.sep}" not in tr] return clean_stacktrace(traceback_stack, user_traceback[::-1], user_dir) def _fix_python_syntax_error(str_trace: str) -> str: - python_traceback_initial_phrase = 'Traceback (most recent call last):' + python_traceback_initial_phrase = "Traceback (most recent call last):" python_traceback_start = ' File "' - is_python_syntax_error = 'SyntaxError' in str_trace and ( - f'\n{python_traceback_start}' in str_trace or - str_trace.startswith(python_traceback_start) + is_python_syntax_error = "SyntaxError" in str_trace and ( + f"\n{python_traceback_start}" in str_trace or str_trace.startswith(python_traceback_start) ) if is_python_syntax_error and python_traceback_initial_phrase not in str_trace: - str_trace = python_traceback_initial_phrase + '\n' + str_trace + str_trace = python_traceback_initial_phrase + "\n" + str_trace return str_trace @@ -72,23 +72,23 @@ def str_to_stacktrace(str_trace: str) -> str: str_trace = _fix_python_syntax_error(str_trace) lines = str_trace.splitlines() - traceback_lines = [i for i, line in enumerate(lines) if line.startswith(' File ')] + traceback_lines = [i for i, line in enumerate(lines) if line.startswith(" File ")] if len(traceback_lines) < 1: return str_trace traceback_stack = [] - for line_from, line_to in zip(traceback_lines, traceback_lines[1:]): - actual_lines = lines[line_from: line_to] + for line_from, line_to in itertools.pairwise(traceback_lines): + actual_lines = lines[line_from:line_to] needed_lines = [line for line in actual_lines if line.startswith(" ")] - traceback_stack += ['\n'.join(needed_lines) + '\n'] + traceback_stack += ["\n".join(needed_lines) + "\n"] - last_traceback = '' - before = '\n'.join(lines[:traceback_lines[0]]) + '\n' - after = '' + last_traceback = "" + before = "\n".join(lines[: traceback_lines[0]]) + "\n" + after = "" - for line in lines[traceback_lines[-1]:]: + for line in lines[traceback_lines[-1] :]: if not after and line.startswith(" "): last_traceback += line + "\n" else: @@ -98,7 +98,7 @@ def str_to_stacktrace(str_trace: str) -> str: user_traceback = [] for trace in traceback_stack: - r''' + r""" Avoid traceback elements such as: File "C:\Users\**\JetBrains\**\plugins\python\helpers\pydev\pydevd.py", line 1477, in _exec @@ -107,26 +107,27 @@ def str_to_stacktrace(str_trace: str) -> str: exec(compile(contents+"\n", file, 'exec'), glob, loc) Which will appear when testing locally inside PyCharm. - ''' # noqa: W291, W505, E501 - if f'{os.sep}JetBrains{os.sep}' in trace: + """ # noqa: W291, 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 +136,18 @@ def clean_stacktrace(full_traceback: List[str], except ValueError: continue - user_file = tr[start_index: end_index] + user_file = tr[start_index:end_index] - if user_file.startswith('<'): + if user_file.startswith("<"): continue - dir_name = os.path.dirname(tr[start_index: end_index]) + dir_name = os.path.dirname(tr[start_index:end_index]) if os.path.isdir(dir_name): dir_names += [os.path.abspath(dir_name)] if dir_names: from hstest.common.os_utils import is_windows + if is_windows(): drives = {} for dir_name in dir_names: @@ -154,28 +156,28 @@ def clean_stacktrace(full_traceback: List[str], if len(drives) > 1: max_drive = max(drives.values()) - drive_to_leave = [d for d in drives if drives[d] == max_drive][0] + drive_to_leave = next(d for d in drives if drives[d] == max_drive) dir_names = [d for d in dir_names if d.startswith(drive_to_leave)] user_dir = os.path.commonpath(dir_names) + os.sep cleaned_traceback = [] for trace in full_traceback[1:-1]: - if trace.startswith(' ' * 4): + if trace.startswith(" " * 4): # Trace line that starts with 4 is a line with SyntaxError cleaned_traceback += [trace] - elif user_dir in trace or ('<' in trace and '>' in trace and '" in trace and " lines that are always in the stacktrace # but include , because it's definitely user's code - if not user_dir.startswith('<'): + if not user_dir.startswith("<"): if user_dir in trace: - trace = trace.replace(user_dir, '') + trace = trace.replace(user_dir, "") else: folder_name = os.path.basename(user_dir[:-1]) if folder_name in trace: index = trace.index(folder_name) - trace = ' File "' + trace[index + len(folder_name + os.sep):] + trace = ' File "' + trace[index + len(folder_name + os.sep) :] cleaned_traceback += [trace] - return full_traceback[0] + ''.join(cleaned_traceback) + full_traceback[-1] + return full_traceback[0] + "".join(cleaned_traceback) + full_traceback[-1] diff --git a/hstest/common/utils.py b/hstest/common/utils.py index 2f85acdc..49acea4d 100644 --- a/hstest/common/utils.py +++ b/hstest/common/utils.py @@ -1,37 +1,37 @@ +from __future__ import annotations + from time import sleep -from typing import Callable +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable -failed_msg_start = '#educational_plugin FAILED + ' -failed_msg_continue = '#educational_plugin ' -success_msg = '#educational_plugin test OK' +failed_msg_start = "#educational_plugin FAILED + " +failed_msg_continue = "#educational_plugin " +success_msg = "#educational_plugin test OK" def failed(message: str, is_unittest: bool): - """ Reports failure """ + """Reports failure.""" if not is_unittest: lines = message.splitlines() - print('\n' + failed_msg_start + lines[0]) - for line in lines[1:]: - print(failed_msg_continue + line) + for _line in lines[1:]: + pass return -1, message def passed(is_unittest: bool): - """ Reports success """ + """Reports success.""" if not is_unittest: - print('\n' + success_msg) - return 0, 'test OK' + pass + return 0, "test OK" def clean_text(text: str) -> str: - return ( - text.replace('\r\n', '\n') - .replace('\r', '\n') - .replace('\u00a0', '\u0020') - ) + return text.replace("\r\n", "\n").replace("\r", "\n").replace("\u00a0", "\u0020") -def try_many_times(times_to_try: int, sleep_time_ms: int, exit_func: Callable[[], bool]): +def try_many_times(times_to_try: int, sleep_time_ms: int, exit_func: Callable[[], bool]) -> bool: while times_to_try > 0: times_to_try -= 1 if exit_func(): diff --git a/hstest/dynamic/__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..344b69d4 100644 --- a/hstest/dynamic/dynamic_test.py +++ b/hstest/dynamic/dynamic_test.py @@ -1,23 +1,26 @@ +from __future__ import annotations + import inspect -from typing import Any, Dict, List +from typing import Any from hstest.stage_test import StageTest from hstest.test_case.test_case import DEFAULT_TIME_LIMIT -def dynamic_test(func=None, *, - order: int = 0, - time_limit: int = DEFAULT_TIME_LIMIT, - data: List[Any] = None, - feedback: str = "", - repeat: int = 1, - files: Dict[str, str] = None): - """ - Decorator for creating dynamic tests - """ +def dynamic_test( + func=None, + *, + order: int = 0, + time_limit: int = DEFAULT_TIME_LIMIT, + data: list[Any] | None = None, + feedback: str = "", + repeat: int = 1, + files: dict[str, str] | None = None, +): + """Decorator for creating dynamic tests.""" class DynamicTestingMethod: - def __init__(self, fn): + def __init__(self, fn) -> None: self.fn = fn def __set_name__(self, owner, name): @@ -32,17 +35,18 @@ def __set_name__(self, owner, name): return from hstest.dynamic.input.dynamic_testing import DynamicTestElement - methods: List[DynamicTestElement] = owner.dynamic_methods() + + methods: list[DynamicTestElement] = owner.dynamic_methods() methods += [ DynamicTestElement( - test=lambda *a, **k: self.fn(*a, **k), + test=self.fn, name=self.fn.__name__, order=(order, len(methods)), repeat=repeat, time_limit=time_limit, feedback=feedback, data=data, - files=files + files=files, ) ] diff --git a/hstest/dynamic/input/__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..f2b7b790 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,6 @@ def _eject_next_input(self): new_input = clean_text(new_input) - if new_input.endswith('\n'): - new_input = new_input[:-1] + new_input = new_input.removesuffix("\n") - self._input_lines += new_input.split('\n') + self._input_lines += new_input.split("\n") diff --git a/hstest/dynamic/input/dynamic_testing.py b/hstest/dynamic/input/dynamic_testing.py index 7cbd1f8b..774b0c59 100644 --- a/hstest/dynamic/input/dynamic_testing.py +++ b/hstest/dynamic/input/dynamic_testing.py @@ -1,53 +1,60 @@ +from __future__ import annotations + import typing -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any from hstest.common.utils import clean_text from hstest.exception.outcomes import TestPassed, UnexpectedError, WrongAnswer from hstest.testing.tested_program import TestedProgram if typing.TYPE_CHECKING: + from collections.abc import Callable + from hstest import CheckResult, StageTest, TestCase from hstest.dynamic.input.dynamic_input_func import DynamicInputFunction - DynamicTesting = Callable[[], Optional[CheckResult]] - DynamicTestingWithoutParams = Callable[[StageTest, Any], Optional[CheckResult]] + DynamicTesting = Callable[[], CheckResult | None] + DynamicTestingWithoutParams = Callable[[StageTest, Any], CheckResult | None] class DynamicTestElement: - def __init__(self, - test: 'DynamicTestingWithoutParams', - name: str, - order: Tuple[int, int], - repeat: int, - time_limit: int, - feedback: str, - data: List[Any], - files: Dict[str, str]): + def __init__( + self, + test: DynamicTestingWithoutParams, + name: str, + order: tuple[int, int], + repeat: int, + time_limit: int, + feedback: str, + data: list[Any], + files: dict[str, str], + ) -> None: self.test: DynamicTestingWithoutParams = test - self.name: str = f"Data passed to dynamic method \"{name}\"" + self.name: str = f'Data passed to dynamic method "{name}"' self.method_name = name - self.order: Tuple[int, int] = order + self.order: tuple[int, int] = order self.repeat: int = repeat self.time_limit: int = time_limit self.feedback: str = feedback - self.data: Optional[List[Any]] = data - self.files: Optional[Dict[str, str]] = files - self.args_list: Optional[List[List[Any]]] = None + self.data: list[Any] | None = data + self.files: dict[str, str] | None = files + self.args_list: list[list[Any]] | None = None - def extract_parametrized_data(self): + def extract_parametrized_data(self) -> None: if self.data is None: self.data = [[]] - if type(self.data) not in [list, tuple]: - raise UnexpectedError(f"{self.name} should be of type " - f"\"list\" or \"tuple\", found {type(self.data)}.") + if type(self.data) not in {list, tuple}: + msg = f"{self.name} should be of type " f'"list" or "tuple", found {type(self.data)}.' + raise UnexpectedError(msg) if len(self.data) == 0: - raise UnexpectedError(f"{self.name} should not be empty.") + msg = f"{self.name} should not be empty." + raise UnexpectedError(msg) found_lists_inside = True for obj in self.data: - if type(obj) not in [list, tuple]: + if type(obj) not in {list, tuple}: found_lists_inside = False break @@ -56,49 +63,57 @@ def extract_parametrized_data(self): else: self.args_list = [[obj] for obj in self.data] - def check_errors(self): + def check_errors(self) -> None: if self.repeat < 0: - raise UnexpectedError(f'Dynamic test "{self.method_name}" ' - f'should not be repeated < 0 times, found {self.repeat}') + msg = ( + f'Dynamic test "{self.method_name}" ' + f"should not be repeated < 0 times, found {self.repeat}" + ) + raise UnexpectedError(msg) if self.files is not None: if type(self.files) != dict: - raise UnexpectedError(f"'Files' parameter in dynamic test should be of type " - f"\"dict\", found {type(self.files)}.") + msg = ( + f"'Files' parameter in dynamic test should be of type " + f'"dict", found {type(self.files)}.' + ) + raise UnexpectedError(msg) for k, v in self.files.items(): if type(k) != str: - raise UnexpectedError( + msg = ( f"All keys in 'files' parameter in dynamic test should be " - f"of type \"str\", found {type(k)}." + f'of type "str", found {type(k)}.' ) + raise UnexpectedError(msg) if type(v) != str: - raise UnexpectedError( + msg = ( f"All values in 'files' parameter in dynamic test should be " - f"of type \"str\", found {type(v)}." + f'of type "str", found {type(v)}.' ) + raise UnexpectedError(msg) - def get_tests(self, obj) -> List['DynamicTesting']: + def get_tests(self, obj) -> list[DynamicTesting]: tests = [] - for i in range(self.repeat): + for _i in range(self.repeat): for args in self.args_list: tests += [lambda o=obj, a=args: self.test(o, *a)] return tests -def to_dynamic_testing(source: str, args: List[str], - input_funcs: List['DynamicInputFunction']) -> 'DynamicTesting': +def to_dynamic_testing( + source: str, args: list[str], input_funcs: list[DynamicInputFunction] +) -> DynamicTesting: from hstest.dynamic.input.dynamic_input_func import DynamicInputFunction from hstest.test_case.check_result import CheckResult class InputFunctionHandler: - def __init__(self, funcs: List[DynamicInputFunction]): - self.input_funcs: List[DynamicInputFunction] = [] + def __init__(self, funcs: list[DynamicInputFunction]) -> None: + self.input_funcs: list[DynamicInputFunction] = [] for func in funcs: - self.input_funcs += [ - DynamicInputFunction(func.trigger_count, func.input_function)] + self.input_funcs += [DynamicInputFunction(func.trigger_count, func.input_function)] - def eject_next_input(self, curr_output: str) -> Optional[str]: + def eject_next_input(self, curr_output: str) -> str | None: if len(self.input_funcs) == 0: return None @@ -109,22 +124,23 @@ def eject_next_input(self, curr_output: str) -> Optional[str]: next_func = input_function.input_function - new_input: Optional[str] + new_input: str | None try: obj = next_func(curr_output) if isinstance(obj, str) or obj is None: new_input = obj elif isinstance(obj, CheckResult): if obj.is_correct: - raise TestPassed() - else: - raise WrongAnswer(obj.feedback) + raise TestPassed + raise WrongAnswer(obj.feedback) else: raise UnexpectedError( - 'Dynamic input should return ' + - f'str or CheckResult objects only. Found: {type(obj)}') + "Dynamic input should return " + + f"str or CheckResult objects only. Found: {type(obj)}" + ) except BaseException as ex: from hstest.stage_test import StageTest + StageTest.curr_test_run.set_error_in_test(ex) return None @@ -136,7 +152,7 @@ def eject_next_input(self, curr_output: str) -> Optional[str]: return new_input - def dynamic_testing_function() -> Optional[CheckResult]: + def dynamic_testing_function() -> CheckResult | None: program = TestedProgram(source) output: str = program.start(*args) @@ -154,15 +170,16 @@ def dynamic_testing_function() -> Optional[CheckResult]: return dynamic_testing_function -def search_dynamic_tests(obj: 'StageTest') -> List['TestCase']: +def search_dynamic_tests(obj: StageTest) -> list[TestCase]: from hstest.test_case.test_case import TestCase - methods: List[DynamicTestElement] = obj.dynamic_methods() + + methods: list[DynamicTestElement] = obj.dynamic_methods() for m in methods: m.extract_parametrized_data() m.check_errors() - tests: List[TestCase] = [] + tests: list[TestCase] = [] for dte in sorted(methods, key=lambda x: x.order): for test in dte.get_tests(obj): @@ -171,7 +188,7 @@ def search_dynamic_tests(obj: 'StageTest') -> List['TestCase']: dynamic_testing=test, time_limit=dte.time_limit, feedback=dte.feedback, - files=dte.files + files=dte.files, ) ] diff --git a/hstest/dynamic/input/input_handler.py b/hstest/dynamic/input/input_handler.py index dd6c3421..42c702e2 100644 --- a/hstest/dynamic/input/input_handler.py +++ b/hstest/dynamic/input/input_handler.py @@ -1,10 +1,13 @@ -import io +from __future__ import annotations + import sys from typing import Any, TYPE_CHECKING from hstest.dynamic.input.input_mock import InputMock if TYPE_CHECKING: + import io + from hstest.dynamic.input.dynamic_input_func import DynamicTestFunction from hstest.dynamic.input.input_mock import Condition @@ -14,17 +17,19 @@ class InputHandler: mock_in: InputMock = InputMock() @staticmethod - def replace_input(): + def replace_input() -> None: sys.stdin = InputHandler.mock_in @staticmethod - def revert_input(): + def revert_input() -> None: sys.stdin = InputHandler.real_in @staticmethod - def install_input_handler(obj: Any, condition: 'Condition', input_func: 'DynamicTestFunction'): + def install_input_handler( + obj: Any, condition: Condition, input_func: DynamicTestFunction + ) -> None: InputHandler.mock_in.install_input_handler(obj, condition, input_func) @staticmethod - def uninstall_input_handler(obj: Any): + def uninstall_input_handler(obj: Any) -> None: InputHandler.mock_in.uninstall_input_handler(obj) diff --git a/hstest/dynamic/input/input_mock.py b/hstest/dynamic/input/input_mock.py index 44533385..3447c7f2 100644 --- a/hstest/dynamic/input/input_mock.py +++ b/hstest/dynamic/input/input_mock.py @@ -1,39 +1,43 @@ -from typing import Any, Callable, Dict, TYPE_CHECKING +from __future__ import annotations + +from typing import Any, TYPE_CHECKING from hstest.dynamic.input.dynamic_input_handler import DynamicInputHandler from hstest.dynamic.security.exit_exception import ExitException -from hstest.dynamic.security.thread_group import ThreadGroup from hstest.exception.outcomes import OutOfInputError, UnexpectedError from hstest.testing.settings import Settings if TYPE_CHECKING: + from collections.abc import Callable + from hstest.dynamic.input.dynamic_input_func import DynamicTestFunction + from hstest.dynamic.security.thread_group import ThreadGroup Condition = Callable[[], bool] class ConditionalInputHandler: - def __init__(self, condition: 'Condition', handler: DynamicInputHandler): + def __init__(self, condition: Condition, handler: DynamicInputHandler) -> None: self.condition = condition self.handler = handler class InputMock: - def __init__(self): - self.handlers: Dict[ThreadGroup, ConditionalInputHandler] = {} + def __init__(self) -> None: + self.handlers: dict[ThreadGroup, ConditionalInputHandler] = {} - def install_input_handler(self, obj: Any, condition: 'Condition', - input_func: 'DynamicTestFunction'): + def install_input_handler( + self, obj: Any, condition: Condition, input_func: DynamicTestFunction + ) -> None: if obj in self.handlers: - raise UnexpectedError("Cannot install input handler from the same program twice") - self.handlers[obj] = ConditionalInputHandler( - condition, - DynamicInputHandler(input_func) - ) + msg = "Cannot install input handler from the same program twice" + raise UnexpectedError(msg) + self.handlers[obj] = ConditionalInputHandler(condition, DynamicInputHandler(input_func)) - def uninstall_input_handler(self, obj: Any): + def uninstall_input_handler(self, obj: Any) -> None: if obj not in self.handlers: - raise UnexpectedError("Cannot uninstall input handler that doesn't exist") + msg = "Cannot uninstall input handler that doesn't exist" + raise UnexpectedError(msg) del self.handlers[obj] def __get_input_handler(self) -> DynamicInputHandler: @@ -42,17 +46,20 @@ def __get_input_handler(self) -> DynamicInputHandler: return handler.handler from hstest import StageTest - StageTest.curr_test_run.set_error_in_test(UnexpectedError( - "Cannot find input handler to read data")) - raise ExitException() + + StageTest.curr_test_run.set_error_in_test( + UnexpectedError("Cannot find input handler to read data") + ) + raise ExitException def readline(self) -> str: line = self.__get_input_handler().eject_next_line() if line is None: if not Settings.allow_out_of_input: from hstest import StageTest + StageTest.curr_test_run.set_error_in_test(OutOfInputError()) - raise ExitException() - else: - raise EOFError('EOF when reading a line') + raise ExitException + msg = "EOF when reading a line" + raise EOFError(msg) return line diff --git a/hstest/dynamic/output/__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..98004fcc 100644 --- a/hstest/dynamic/output/output_handler.py +++ b/hstest/dynamic/output/output_handler.py @@ -1,4 +1,5 @@ -import io +from __future__ import annotations + import sys from typing import Any, TYPE_CHECKING @@ -8,6 +9,8 @@ from hstest.dynamic.security.thread_group import ThreadGroup if TYPE_CHECKING: + import io + from hstest.dynamic.input.input_mock import Condition @@ -19,29 +22,26 @@ class OutputHandler: _mock_err: OutputMock = None @staticmethod - def print(obj): + def print(obj) -> None: if True: return - lines = obj.strip().split('\n') + lines = obj.strip().split("\n") group = ThreadGroup.curr_group() - if group: - name = group.name - else: - name = "Root" + name = group.name if group else "Root" - prepend = f'[{name}] ' + prepend = f"[{name}] " - output = prepend + ('\n' + prepend).join(lines) - full = BLUE + output + '\n' + RESET + output = prepend + ("\n" + prepend).join(lines) + full = BLUE + output + "\n" + RESET if group: OutputHandler.get_real_out().write(full) OutputHandler.get_real_out().flush() else: - print(full, end='') + pass @staticmethod def get_real_out() -> io.TextIOWrapper: @@ -52,7 +52,7 @@ def get_real_err() -> io.TextIOWrapper: return OutputHandler._mock_err.original @staticmethod - def replace_stdout(): + def replace_stdout() -> None: OutputHandler._real_out = sys.stdout OutputHandler._real_err = sys.stderr @@ -63,13 +63,13 @@ def replace_stdout(): sys.stderr = OutputHandler._mock_err @staticmethod - def revert_stdout(): + def revert_stdout() -> None: OutputHandler.reset_output() sys.stdout = OutputHandler._real_out sys.stderr = OutputHandler._real_err @staticmethod - def reset_output(): + def reset_output() -> None: OutputHandler._mock_out.reset() OutputHandler._mock_err.reset() @@ -90,18 +90,19 @@ def get_partial_output(obj: Any) -> str: return clean_text(OutputHandler._mock_out.partial(obj)) @staticmethod - def inject_input(user_input: str): + def inject_input(user_input: str) -> None: from hstest.stage_test import StageTest + if StageTest.curr_test_run is not None: StageTest.curr_test_run.set_input_used() OutputHandler._mock_out.inject_input(user_input) @staticmethod - def install_output_handler(obj: Any, condition: 'Condition'): + def install_output_handler(obj: Any, condition: Condition) -> None: OutputHandler._mock_out.install_output_handler(obj, condition) OutputHandler._mock_err.install_output_handler(obj, condition) @staticmethod - def uninstall_output_handler(obj: Any): + def uninstall_output_handler(obj: Any) -> None: OutputHandler._mock_out.uninstall_output_handler(obj) OutputHandler._mock_err.uninstall_output_handler(obj) diff --git a/hstest/dynamic/output/output_mock.py b/hstest/dynamic/output/output_mock.py index e21263e8..38303c9a 100644 --- a/hstest/dynamic/output/output_mock.py +++ b/hstest/dynamic/output/output_mock.py @@ -1,5 +1,6 @@ -import io -from typing import Any, Dict, List, TYPE_CHECKING +from __future__ import annotations + +from typing import Any, TYPE_CHECKING from hstest.dynamic.output.colored_output import BLUE, RESET from hstest.dynamic.output.infinite_loop_detector import loop_detector @@ -8,19 +9,20 @@ from hstest.testing.settings import Settings if TYPE_CHECKING: + import io + from hstest.dynamic.input.input_mock import Condition class ConditionalOutput: - def __init__(self, condition: 'Condition'): + def __init__(self, condition: Condition) -> None: self.condition = condition - self.output: List[str] = [] + self.output: list[str] = [] class OutputMock: - """ - original stream is used to actually see - the test in the console and nothing else + """original stream is used to actually see + the test in the console and nothing else. cloned stream is used to collect all output from the test and redirect to check function @@ -32,25 +34,25 @@ class OutputMock: but also injected input from the test """ - def __init__(self, real_out: io.TextIOWrapper, is_stderr: bool = False): + def __init__(self, real_out: io.TextIOWrapper, is_stderr: bool = False) -> None: class RealOutputMock: - def __init__(self, out: io.TextIOWrapper): + def __init__(self, out: io.TextIOWrapper) -> None: self.out = out - def write(self, text): + def write(self, text) -> None: if not ignore_stdout: self.out.write(text) - def flush(self): + def flush(self) -> None: self.out.flush() - def close(self): + def close(self) -> None: self.out.close() self._original: RealOutputMock = RealOutputMock(real_out) - self._cloned: List[str] = [] # used in check function - self._dynamic: List[str] = [] # used to append inputs - self._partial: Dict[Any, ConditionalOutput] = {} # separated outputs for each program + self._cloned: list[str] = [] # used in check function + self._dynamic: list[str] = [] # used to append inputs + self._partial: dict[Any, ConditionalOutput] = {} # separated outputs for each program self._is_stderr = is_stderr @property @@ -59,19 +61,19 @@ def original(self): @property def cloned(self) -> str: - return ''.join(self._cloned) + return "".join(self._cloned) @property def dynamic(self) -> str: - return ''.join(self._dynamic) + return "".join(self._dynamic) def partial(self, obj: Any) -> str: output = self._partial[obj].output - result = ''.join(output) + result = "".join(output) output.clear() return result - def write(self, text): + def write(self, text) -> None: partial_handler = self.__get_partial_handler() if partial_handler is None: @@ -86,34 +88,36 @@ def write(self, text): loop_detector.write(text) - def getvalue(self): + def getvalue(self) -> None: pass - def flush(self): + def flush(self) -> None: self._original.flush() - def close(self): + def close(self) -> None: self._original.close() - def inject_input(self, user_input: str): + def inject_input(self, user_input: str) -> None: self._original.write(user_input) self._dynamic.append(user_input) - def reset(self): + def reset(self) -> None: self._cloned.clear() self._dynamic.clear() for value in self._partial.values(): value.output.clear() loop_detector.reset() - def install_output_handler(self, obj: Any, condition: 'Condition'): + def install_output_handler(self, obj: Any, condition: Condition) -> None: if obj in self._partial: - raise UnexpectedError("Cannot install output handler from the same program twice") + msg = "Cannot install output handler from the same program twice" + raise UnexpectedError(msg) self._partial[obj] = ConditionalOutput(condition) - def uninstall_output_handler(self, obj: Any): + def uninstall_output_handler(self, obj: Any) -> None: if obj not in self._partial: - raise UnexpectedError("Cannot uninstall output handler that doesn't exist") + msg = "Cannot uninstall output handler that doesn't exist" + raise UnexpectedError(msg) del self._partial[obj] def __get_partial_handler(self): diff --git a/hstest/dynamic/security/__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..dccbd3b2 100644 --- a/hstest/dynamic/security/thread_group.py +++ b/hstest/dynamic/security/thread_group.py @@ -1,23 +1,25 @@ +from __future__ import annotations + from threading import current_thread, Thread -from typing import List, Optional class ThreadGroup: - def __init__(self, name: str = None): + def __init__(self, name: str | None = None) -> None: if name: self._name: str = name else: from hstest import StageTest + test_num = StageTest.curr_test_global - self._name = f'Test {test_num}' + self._name = f"Test {test_num}" - self.threads: List[Thread] = [] + self.threads: list[Thread] = [] curr = current_thread() if hasattr(curr, "_group"): - self._parent: Optional[ThreadGroup] = curr._group + self._parent: ThreadGroup | None = curr._group else: - self._parent: Optional[ThreadGroup] = None + self._parent: ThreadGroup | None = None @property def name(self): @@ -27,9 +29,9 @@ def name(self): def parent(self): return self._parent - def add(self, thread: Thread): + def add(self, thread: Thread) -> None: self.threads.append(thread) @staticmethod - def curr_group() -> 'ThreadGroup': - return getattr(current_thread(), '_group', None) + def curr_group() -> ThreadGroup: + return getattr(current_thread(), "_group", None) diff --git a/hstest/dynamic/security/thread_handler.py b/hstest/dynamic/security/thread_handler.py index 469b8ddb..5c2d701e 100644 --- a/hstest/dynamic/security/thread_handler.py +++ b/hstest/dynamic/security/thread_handler.py @@ -1,24 +1,28 @@ +from __future__ import annotations + from threading import current_thread, Thread -from typing import Callable, Optional +from typing import TYPE_CHECKING from hstest.dynamic.security.thread_group import ThreadGroup +if TYPE_CHECKING: + from collections.abc import Callable -class ThreadHandler: +class ThreadHandler: _group = None - _old_init: Optional[Callable[[], Thread]] = None + _old_init: Callable[[], Thread] | None = None @classmethod - def install_thread_group(cls): + def install_thread_group(cls) -> None: if cls._old_init is None: cls._old_init = Thread.__init__ Thread.__init__ = ThreadHandler.init - cls._group = ThreadGroup('Main') + cls._group = ThreadGroup("Main") current_thread()._group = cls._group @classmethod - def uninstall_thread_group(cls): + def uninstall_thread_group(cls) -> None: if cls._old_init is not None: Thread.__init__ = cls._old_init cls._old_init = None @@ -26,9 +30,9 @@ def uninstall_thread_group(cls): cls._group = None @staticmethod - def init(self, group=None, target=None, name=None, - args=(), kwargs=None, *, daemon=None): - + def init( + self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None + ) -> None: ThreadHandler._old_init(self, None, target, name, args, kwargs, daemon=daemon) # Custom addition to Thread class (implement thread groups) @@ -37,7 +41,7 @@ def init(self, group=None, target=None, name=None, else: curr = current_thread() - if hasattr(curr, '_group'): + if hasattr(curr, "_group"): self._group = curr._group else: self._group = ThreadGroup(self._name) diff --git a/hstest/dynamic/system_handler.py b/hstest/dynamic/system_handler.py index 4b40fc9d..09f50cc6 100644 --- a/hstest/dynamic/system_handler.py +++ b/hstest/dynamic/system_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from threading import current_thread, Lock from typing import Any, TYPE_CHECKING @@ -18,7 +20,7 @@ class SystemHandler: __locker_thread = None @staticmethod - def set_up(): + def set_up() -> None: SystemHandler._lock_system_for_testing() OutputHandler.replace_stdout() @@ -27,7 +29,7 @@ def set_up(): ThreadHandler.install_thread_group() @staticmethod - def tear_down(): + def tear_down() -> None: SystemHandler._unlock_system_for_testing() OutputHandler.revert_stdout() @@ -36,33 +38,33 @@ def tear_down(): ThreadHandler.uninstall_thread_group() @staticmethod - def _lock_system_for_testing(): + def _lock_system_for_testing() -> None: with SystemHandler.__lock: if SystemHandler.__locked: - raise ErrorWithFeedback( - "Cannot start the testing process more than once") + msg = "Cannot start the testing process more than once" + raise ErrorWithFeedback(msg) SystemHandler.__locked = True SystemHandler.__locker_thread = current_thread() @staticmethod - def _unlock_system_for_testing(): + def _unlock_system_for_testing() -> None: if current_thread() != SystemHandler.__locker_thread: - raise ErrorWithFeedback( - "Cannot tear down the testing process from the other thread") + msg = "Cannot tear down the testing process from the other thread" + raise ErrorWithFeedback(msg) with SystemHandler.__lock: if not SystemHandler.__locked: - raise ErrorWithFeedback( - "Cannot tear down the testing process more than once") + msg = "Cannot tear down the testing process more than once" + raise ErrorWithFeedback(msg) SystemHandler.__locked = False SystemHandler.__locker_thread = None @staticmethod - def install_handler(obj: Any, condition: 'Condition', input_func: 'DynamicTestFunction'): + def install_handler(obj: Any, condition: Condition, input_func: DynamicTestFunction) -> None: InputHandler.install_input_handler(obj, condition, input_func) OutputHandler.install_output_handler(obj, condition) @staticmethod - def uninstall_handler(obj: Any): + def uninstall_handler(obj: Any) -> None: InputHandler.uninstall_input_handler(obj) OutputHandler.uninstall_output_handler(obj) diff --git a/hstest/exception/__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..7000582d 100644 --- a/hstest/exception/outcomes.py +++ b/hstest/exception/outcomes.py @@ -1,4 +1,4 @@ -from typing import Optional +from __future__ import annotations class OutcomeError(BaseException): @@ -6,35 +6,35 @@ class OutcomeError(BaseException): class SyntaxException(OutcomeError): - def __init__(self, exception: SyntaxError, file: str): + def __init__(self, exception: SyntaxError, file: str) -> None: self.file: str = file self.exception: SyntaxError = exception class ExceptionWithFeedback(OutcomeError): - def __init__(self, error_text: str, real_exception: Optional[BaseException]): + def __init__(self, error_text: str, real_exception: BaseException | None) -> None: self.error_text: str = error_text self.real_exception: BaseException = real_exception class ErrorWithFeedback(OutcomeError): - def __init__(self, error_text: str): + def __init__(self, error_text: str) -> None: self.error_text = error_text class OutOfInputError(ErrorWithFeedback): - def __init__(self): - super().__init__('Program ran out of input. You tried to read more than expected.') + def __init__(self) -> None: + super().__init__("Program ran out of input. You tried to read more than expected.") class UnexpectedError(OutcomeError): - def __init__(self, error_text: str, ex: Optional[BaseException] = None): + def __init__(self, error_text: str, ex: BaseException | None = None) -> None: self.error_text = error_text self.exception = ex class CompilationError(OutcomeError): - def __init__(self, error_text: str): + def __init__(self, error_text: str) -> None: self.error_text = error_text @@ -43,5 +43,5 @@ class TestPassed(OutcomeError): class WrongAnswer(OutcomeError): - def __init__(self, feedback: str): + def __init__(self, feedback: str) -> None: self.feedback = feedback diff --git a/hstest/exception/testing.py b/hstest/exception/testing.py index 19fb0ed9..dd9dba3f 100644 --- a/hstest/exception/testing.py +++ b/hstest/exception/testing.py @@ -1,6 +1,8 @@ +from __future__ import annotations + class TimeLimitException(BaseException): - def __init__(self, time_limit_ms: int): + def __init__(self, time_limit_ms: int) -> None: self.time_limit_ms: int = time_limit_ms @@ -13,7 +15,7 @@ class TestedProgramFinishedEarly(BaseException): class InfiniteLoopException(BaseException): - def __init__(self, message: str): + def __init__(self, message: str) -> None: self.message = message diff --git a/hstest/exceptions.py b/hstest/exceptions.py index 2c54a85c..1f64f026 100644 --- a/hstest/exceptions.py +++ b/hstest/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hstest.exception.outcomes import TestPassed, WrongAnswer # deprecated, but have to be sure old tests work as expected diff --git a/hstest/outcomes/__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..fdf3d78e 100644 --- a/hstest/outcomes/error_outcome.py +++ b/hstest/outcomes/error_outcome.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hstest.common.reflection_utils import get_stacktrace from hstest.exception.outcomes import ErrorWithFeedback from hstest.exception.testing import FileDeletionError, InfiniteLoopException, TimeLimitException @@ -5,7 +7,7 @@ class ErrorOutcome(Outcome): - def __init__(self, test_num: int, cause: BaseException): + def __init__(self, test_num: int, cause: BaseException) -> None: super().__init__() self.test_number = test_num @@ -22,28 +24,30 @@ def __init__(self, test_num: int, cause: BaseException): self.error_text = "Infinite loop detected.\n" + cause.message elif isinstance(cause, KeyboardInterrupt): - self.error_text = "It seems like you've stopped the testing process forcibly.\n" \ - "If this is not the case, please report this issue to support" + self.error_text = ( + "It seems like you've stopped the testing process forcibly.\n" + "If this is not the case, please report this issue to support" + ) self.stack_trace = get_stacktrace(cause, hide_internals=False) - def _init_permission_error(self, _: FileDeletionError): + def _init_permission_error(self, _: FileDeletionError) -> None: self.error_text = ( - "The file you opened " + - "can't be deleted after the end of the test. " + - "Probably you didn't close it." + "The file you opened " + + "can't be deleted after the end of the test. " + + "Probably you didn't close it." ) - def _init_time_limit_exception(self, ex: TimeLimitException): + def _init_time_limit_exception(self, ex: TimeLimitException) -> None: time_limit: int = ex.time_limit_ms - time_unit: str = 'milliseconds' + time_unit: str = "milliseconds" if time_limit > 1999: time_limit //= 1000 - time_unit = 'seconds' + time_unit = "seconds" self.error_text = ( - 'In this test, the program is running for a long time, ' + - f'more than {time_limit} {time_unit}. Most likely, ' + - 'the program has gone into an infinite loop.' + "In this test, the program is running for a long time, " + + f"more than {time_limit} {time_unit}. Most likely, " + + "the program has gone into an infinite loop." ) def get_type(self) -> str: - return 'Error' + return "Error" diff --git a/hstest/outcomes/exception_outcome.py b/hstest/outcomes/exception_outcome.py index cda56014..2216402e 100644 --- a/hstest/outcomes/exception_outcome.py +++ b/hstest/outcomes/exception_outcome.py @@ -1,10 +1,16 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.common.reflection_utils import get_stacktrace, str_to_stacktrace -from hstest.exception.outcomes import ExceptionWithFeedback from hstest.outcomes.outcome import Outcome +if TYPE_CHECKING: + from hstest.exception.outcomes import ExceptionWithFeedback + class ExceptionOutcome(Outcome): - def __init__(self, test_num: int, ex: ExceptionWithFeedback): + def __init__(self, test_num: int, ex: ExceptionWithFeedback) -> None: super().__init__() cause = ex.real_exception feedback = ex.error_text @@ -17,13 +23,13 @@ def __init__(self, test_num: int, ex: ExceptionWithFeedback): else: self.stack_trace = str_to_stacktrace(feedback) - self.error_text = '' + self.error_text = "" - eof = 'EOFError: EOF when reading a line' - eof_feedback = 'Probably your program run out of input (tried to read more than expected)' + eof = "EOFError: EOF when reading a line" + eof_feedback = "Probably your program run out of input (tried to read more than expected)" if self.stack_trace.strip().endswith(eof): - self.error_text += '\n\n' + eof_feedback + self.error_text += "\n\n" + eof_feedback def get_type(self) -> str: - return 'Exception' + return "Exception" diff --git a/hstest/outcomes/outcome.py b/hstest/outcomes/outcome.py index c67591c6..15ca9a37 100644 --- a/hstest/outcomes/outcome.py +++ b/hstest/outcomes/outcome.py @@ -1,36 +1,41 @@ +from __future__ import annotations + from hstest.common.reflection_utils import str_to_stacktrace from hstest.common.utils import clean_text from hstest.dynamic.output.output_handler import OutputHandler from hstest.exception.outcomes import ( - CompilationError, ErrorWithFeedback, ExceptionWithFeedback, WrongAnswer + CompilationError, + ErrorWithFeedback, + ExceptionWithFeedback, + WrongAnswer, ) from hstest.exception.testing import FileDeletionError, InfiniteLoopException, TimeLimitException class Outcome: - def __init__(self, test_number: int = 0, error_text: str = '', stack_trace: str = ''): + def __init__(self, test_number: int = 0, error_text: str = "", stack_trace: str = "") -> None: self.test_number: int = test_number self.error_text: str = error_text self.stack_trace: str = stack_trace def get_type(self) -> str: - raise NotImplementedError() + raise NotImplementedError - def __str__(self): + def __str__(self) -> str: if self.test_number == 0: - when_error_happened = ' during testing' + when_error_happened = " during testing" elif self.test_number > 0: - when_error_happened = f' in test #{self.test_number}' + when_error_happened = f" in test #{self.test_number}" else: - when_error_happened = '' + when_error_happened = "" result = self.get_type() + when_error_happened if self.error_text: - result += '\n\n' + clean_text(self.error_text.strip()) + result += "\n\n" + clean_text(self.error_text.strip()) if self.stack_trace: - result += '\n\n' + clean_text(self.stack_trace.strip()) + result += "\n\n" + clean_text(self.stack_trace.strip()) full_out = OutputHandler.get_dynamic_output() full_err = str_to_stacktrace(OutputHandler.get_err()) @@ -44,10 +49,11 @@ def __str__(self): worth_showing_args = len(arguments.strip()) != 0 from hstest.stage_test import StageTest + test_run = StageTest.curr_test_run if worth_showing_out or worth_showing_err or worth_showing_args: - result += '\n\n' + result += "\n\n" if worth_showing_out or worth_showing_err: result += "Please find below the output of your program during this failed test.\n" if test_run and test_run.input_used: @@ -57,12 +63,12 @@ def __str__(self): result += "\n---\n\n" if worth_showing_args: - result += arguments + '\n\n' + result += arguments + "\n\n" if worth_showing_out: if worth_showing_err: - result += 'stdout:\n' - result += trimmed_out + '\n\n' + result += "stdout:\n" + result += trimmed_out + "\n\n" if worth_showing_err: result += "stderr:\n" + trimmed_err @@ -71,9 +77,10 @@ def __str__(self): @staticmethod def __get_args(): - arguments = '' + arguments = "" from hstest.stage_test import StageTest + test_run = StageTest.curr_test_run if test_run is not None: @@ -81,10 +88,10 @@ def __get_args(): programs_with_args = [p for p in tested_programs if len(p.run_args)] for pr in programs_with_args: - arguments += 'Arguments' + arguments += "Arguments" if len(tested_programs) > 1: - arguments += f' for {pr}' - pr_args = [f'"{arg}"' if ' ' in arg else arg for arg in pr.run_args] + arguments += f" for {pr}" + pr_args = [f'"{arg}"' if " " in arg else arg for arg in pr.run_args] arguments += f': {" ".join(pr_args)}\n' arguments = arguments.strip() @@ -93,17 +100,19 @@ def __get_args(): @staticmethod def __trim_lines(full_out): - result = '' + result = "" max_lines_in_output = 250 lines = full_out.splitlines() is_output_too_long = len(lines) > max_lines_in_output if is_output_too_long: - result += f'[last {max_lines_in_output} lines of output are shown, ' \ - f'{len(lines) - max_lines_in_output} skipped]\n' + result += ( + f"[last {max_lines_in_output} lines of output are shown, " + f"{len(lines) - max_lines_in_output} skipped]\n" + ) last_lines = lines[-max_lines_in_output:] - result += '\n'.join(last_lines) + result += "\n".join(last_lines) else: result += full_out @@ -120,20 +129,20 @@ def get_outcome(ex: BaseException, curr_test: int): if isinstance(ex, WrongAnswer): return WrongAnswerOutcome(curr_test, ex) - elif isinstance(ex, ExceptionWithFeedback): + if isinstance(ex, ExceptionWithFeedback): return ExceptionOutcome(curr_test, ex) - elif isinstance(ex, CompilationError): + if isinstance(ex, CompilationError): return CompilationErrorOutcome(ex) - elif isinstance(ex, ( - ErrorWithFeedback, - FileDeletionError, - TimeLimitException, - InfiniteLoopException, - KeyboardInterrupt - )): + if isinstance( + ex, + ErrorWithFeedback + | FileDeletionError + | TimeLimitException + | InfiniteLoopException + | KeyboardInterrupt, + ): return ErrorOutcome(curr_test, ex) - else: - return UnexpectedErrorOutcome(curr_test, ex) + return UnexpectedErrorOutcome(curr_test, ex) diff --git a/hstest/outcomes/unexpected_error_outcome.py b/hstest/outcomes/unexpected_error_outcome.py index 01103fb5..6bf84364 100644 --- a/hstest/outcomes/unexpected_error_outcome.py +++ b/hstest/outcomes/unexpected_error_outcome.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hstest.common.reflection_utils import get_stacktrace from hstest.exception.failure_handler import get_report from hstest.exception.outcomes import UnexpectedError @@ -5,14 +7,13 @@ class UnexpectedErrorOutcome(Outcome): - def __init__(self, test_num: int, cause: BaseException): + def __init__(self, test_num: int, cause: BaseException) -> None: super().__init__() self.test_number = test_num - self.error_text = 'We have recorded this bug ' \ - 'and will fix it soon.\n\n' + get_report() + self.error_text = "We have recorded this bug " "and will fix it soon.\n\n" + get_report() self.stack_trace = get_stacktrace(cause, hide_internals=False) if isinstance(cause, UnexpectedError) and cause.exception is not None: - self.stack_trace += '\n' + get_stacktrace(cause.exception, hide_internals=False) + self.stack_trace += "\n" + get_stacktrace(cause.exception, hide_internals=False) def get_type(self) -> str: - return 'Unexpected error' + return "Unexpected error" diff --git a/hstest/outcomes/wrong_answer_outcome.py b/hstest/outcomes/wrong_answer_outcome.py index 427a49e1..01df0163 100644 --- a/hstest/outcomes/wrong_answer_outcome.py +++ b/hstest/outcomes/wrong_answer_outcome.py @@ -1,10 +1,16 @@ -from hstest.exception.outcomes import WrongAnswer +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.outcomes.outcome import Outcome +if TYPE_CHECKING: + from hstest.exception.outcomes import WrongAnswer + class WrongAnswerOutcome(Outcome): - def __init__(self, test_num: int, ex: WrongAnswer): - super().__init__(test_num, ex.feedback, '') + def __init__(self, test_num: int, ex: WrongAnswer) -> None: + super().__init__(test_num, ex.feedback, "") def get_type(self) -> str: - return 'Wrong answer' + return "Wrong answer" diff --git a/hstest/stage/__init__.py b/hstest/stage/__init__.py index f76d54f1..f6bf6f3c 100644 --- a/hstest/stage/__init__.py +++ b/hstest/stage/__init__.py @@ -1,9 +1,12 @@ +from __future__ import annotations + +from typing import Any + __all__ = [ - 'StageTest', - 'DjangoTest', - 'FlaskTest', - 'PlottingTest', - 'SQLTest', + "DjangoTest", + "FlaskTest", + "SQLTest", + "StageTest", ] from hstest.stage.django_test import DjangoTest @@ -11,7 +14,12 @@ from hstest.stage.sql_test import SQLTest from hstest.stage.stage_test import StageTest +# Define PlottingTest as Any before trying to import it +PlottingTest: Any + try: from hstest.stage.plotting_test import PlottingTest + + __all__.append("PlottingTest") except ImportError: PlottingTest = None diff --git a/hstest/stage/django_test.py b/hstest/stage/django_test.py index 04a5a044..071fb04e 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,21 @@ class DjangoTest(StageTest): test_database: str = attach.test_database use_database: bool = attach.use_database - def __init__(self, args='', *, source: str = ''): + def __init__(self, args="", *, source: str = "") -> None: super().__init__(args, source=source) self.attach.use_database = self.use_database loop_detector.working = False Settings.do_reset_output = False def read_page(self, link: str) -> str: - """ - Deprecated, use get(...) instead - """ + """Deprecated, use get(...) instead.""" return clean_text(urlopen(link).read().decode()) - def get_url(self, link: str = ''): - if link.startswith('/'): - link = link[1:] - return f'http://localhost:{self.attach.port}/{link}' + def get_url(self, link: str = "") -> str: + link = link.removeprefix("/") + return f"http://localhost:{self.attach.port}/{link}" def get(self, link: str) -> str: - if not link.startswith('http://'): + if not link.startswith("http://"): link = self.get_url(link) return clean_text(urlopen(link).read().decode()) diff --git a/hstest/stage/flask_test.py b/hstest/stage/flask_test.py index f0efdafe..c7b88b53 100644 --- a/hstest/stage/flask_test.py +++ b/hstest/stage/flask_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from urllib.request import urlopen from hstest.common.utils import clean_text @@ -13,7 +15,7 @@ class FlaskTest(StageTest): runner = FlaskApplicationRunner() attach: FlaskSettings = FlaskSettings() - def __init__(self, args='', *, source: str = ''): + def __init__(self, args="", *, source: str = "") -> None: super().__init__(args, source=source) loop_detector.working = False Settings.do_reset_output = False @@ -33,28 +35,30 @@ def __init__(self, args='', *, source: str = ''): else: self.attach.sources += [item] - def get_url(self, link: str = '', *, source: str = None): - if link.startswith('/'): - link = link[1:] + def get_url(self, link: str = "", *, source: str | None = None): + link = link.removeprefix("/") def create_url(port: int) -> str: - return f'http://localhost:{port}/{link}' + return f"http://localhost:{port}/{link}" if len(self.attach.sources) == 1: return create_url(self.attach.sources[0][1]) - elif len(self.attach.sources) == 0: - raise UnexpectedError('Cannot find sources') + if len(self.attach.sources) == 0: + msg = "Cannot find sources" + raise UnexpectedError(msg) sources_fits = [i for i in self.attach.sources if i[0] == source] if len(sources_fits) == 0: - raise UnexpectedError(f'Bad source: {source}') - elif len(sources_fits) > 1: - raise UnexpectedError(f'Multiple sources ({len(sources_fits)}) found: {source}') + msg = f"Bad source: {source}" + raise UnexpectedError(msg) + if len(sources_fits) > 1: + msg = f"Multiple sources ({len(sources_fits)}) found: {source}" + raise UnexpectedError(msg) return create_url(sources_fits[0][1]) - def get(self, link: str, *, source: str = None) -> str: - if not link.startswith('http://'): + def get(self, link: str, *, source: str | None = None) -> str: + if not link.startswith("http://"): link = self.get_url(link, source=source) return clean_text(urlopen(link).read().decode()) diff --git a/hstest/stage/plotting_test.py b/hstest/stage/plotting_test.py index 2be941e4..fcc1e2f2 100644 --- a/hstest/stage/plotting_test.py +++ b/hstest/stage/plotting_test.py @@ -1,22 +1,25 @@ -from typing import List +from __future__ import annotations + +from typing import TYPE_CHECKING from hstest.stage.stage_test import StageTest -from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.runner.plot_testing_runner import PlottingTestingRunner +if TYPE_CHECKING: + from hstest.testing.plotting.drawing.drawing import Drawing -class PlottingTest(StageTest): - def __init__(self, args='', *, source: str = ''): +class PlottingTest(StageTest): + def __init__(self, args="", *, source: str = "") -> None: super().__init__(args, source=source) - self._all_drawings: List[Drawing] = [] - self._new_drawings: List[Drawing] = [] + self._all_drawings: list[Drawing] = [] + self._new_drawings: list[Drawing] = [] self.runner = PlottingTestingRunner(self._all_drawings, self._new_drawings) - def all_figures(self) -> List[Drawing]: + def all_figures(self) -> list[Drawing]: return self._all_drawings - def new_figures(self) -> List[Drawing]: + def new_figures(self) -> list[Drawing]: result = self._new_drawings[:] self._new_drawings.clear() return result diff --git a/hstest/stage/sql_test.py b/hstest/stage/sql_test.py index fa328fd9..615f486e 100644 --- a/hstest/stage/sql_test.py +++ b/hstest/stage/sql_test.py @@ -1,4 +1,4 @@ -from typing import Dict +from __future__ import annotations from hstest.exception.outcomes import WrongAnswer from hstest.stage.stage_test import StageTest @@ -6,10 +6,10 @@ class SQLTest(StageTest): - queries: Dict[str, str] = dict() + queries: dict[str, str] = {} db: any = None - def __init__(self, source: str = ''): + def __init__(self, source: str = "") -> None: super().__init__(source) self.runner = SQLRunner(self) @@ -22,7 +22,8 @@ def execute(self, query_name: str): try: return cursor.execute(self.queries[query_name]) except Exception as ex: - raise WrongAnswer(f"Error while running '{query_name}': \n\n{ex}") + msg = f"Error while running '{query_name}': \n\n{ex}" + raise WrongAnswer(msg) def execute_and_fetch_all(self, query_name: str): return self.execute(query_name).fetchall() diff --git a/hstest/stage/stage_test.py b/hstest/stage/stage_test.py index b2f1b7be..aca1c228 100644 --- a/hstest/stage/stage_test.py +++ b/hstest/stage/stage_test.py @@ -1,6 +1,9 @@ +from __future__ import annotations + +import contextlib import os import unittest -from typing import Any, Dict, List, Optional, Tuple, Type +from typing import Any, TYPE_CHECKING from hstest.common.file_utils import walk_user_files from hstest.common.reflection_utils import is_tests, setup_cwd @@ -12,8 +15,6 @@ from hstest.exception.failure_handler import get_exception_text, get_report from hstest.exception.outcomes import OutcomeError, UnexpectedError, WrongAnswer from hstest.outcomes.outcome import Outcome -from hstest.test_case.check_result import CheckResult -from hstest.test_case.test_case import TestCase from hstest.testing.execution.main_module_executor import MainModuleExecutor from hstest.testing.execution.process.cpp_executor import CppExecutor from hstest.testing.execution.process.go_executor import GoExecutor @@ -22,24 +23,34 @@ from hstest.testing.execution.process.shell_executor import ShellExecutor from hstest.testing.execution_options import force_process_testing from hstest.testing.runner.async_dynamic_testing_runner import AsyncDynamicTestingRunner -from hstest.testing.runner.test_runner import TestRunner from hstest.testing.test_run import TestRun +if TYPE_CHECKING: + from hstest.test_case.check_result import CheckResult + from hstest.test_case.test_case import TestCase + from hstest.testing.runner.test_runner import TestRunner + class DirMeta(type): - def __dir__(self): + def __dir__(cls): from hstest.testing.unittest.expected_fail_test import ExpectedFailTest from hstest.testing.unittest.unexepected_error_test import UnexpectedErrorTest from hstest.testing.unittest.user_error_test import UserErrorTest - if (not issubclass(self, StageTest) or self == StageTest or - self in {ExpectedFailTest, UserErrorTest, UnexpectedErrorTest}): + + if ( + not issubclass(cls, StageTest) + or cls == StageTest + or cls in {ExpectedFailTest, UserErrorTest, UnexpectedErrorTest} + ): return [] - init_dir = dir(super(DirMeta, self)) + list(self.__dict__.keys()) - filtered_dir = list(filter(lambda x: not str(x).startswith('test'), init_dir)) - filtered_dir.append('test_run_unittest') - if (not self.dynamic_methods() and - 'generate' not in init_dir and - not issubclass(self, ExpectedFailTest)): + init_dir = dir(super()) + list(cls.__dict__.keys()) + filtered_dir = list(filter(lambda x: not str(x).startswith("test"), init_dir)) + filtered_dir.append("test_run_unittest") + if ( + not cls.dynamic_methods() + and "generate" not in init_dir + and not issubclass(cls, ExpectedFailTest) + ): return [] return set(filtered_dir) @@ -48,11 +59,11 @@ class StageTest(unittest.TestCase, metaclass=DirMeta): runner: TestRunner = None attach: Any = None source: str = None - curr_test_run: Optional[TestRun] = None + curr_test_run: TestRun | None = None curr_test_global: int = 0 - def __init__(self, args='', *, source: str = ''): - super(StageTest, self).__init__('test_run_unittest') + def __init__(self, args="", *, source: str = "") -> None: + super().__init__("test_run_unittest") self.is_tests = False if self.source: @@ -60,46 +71,46 @@ def __init__(self, args='', *, source: str = ''): else: self.source_name: str = source - def test_run_unittest(self): + def test_run_unittest(self) -> None: result, feedback = self.run_tests(is_unittest=True) if result != 0: self.fail(feedback) - def after_all_tests(self): + def after_all_tests(self) -> None: pass def _init_runner(self) -> TestRunner: - for folder, dirs, files in walk_user_files(os.getcwd()): + for _folder, _dirs, files in walk_user_files(os.getcwd()): for f in files: - if f.endswith('.cpp'): + if f.endswith(".cpp"): return AsyncDynamicTestingRunner(CppExecutor) - if f.endswith('.go'): + if f.endswith(".go"): return AsyncDynamicTestingRunner(GoExecutor) - if f.endswith('.js'): + if f.endswith(".js"): return AsyncDynamicTestingRunner(JavascriptExecutor) - if f.endswith('.sh'): + if f.endswith(".sh"): return AsyncDynamicTestingRunner(ShellExecutor) - if f.endswith('.py'): + if f.endswith(".py"): if force_process_testing: return AsyncDynamicTestingRunner(PythonExecutor) - else: - return AsyncDynamicTestingRunner(MainModuleExecutor) + return AsyncDynamicTestingRunner(MainModuleExecutor) return AsyncDynamicTestingRunner(MainModuleExecutor) - def _init_tests(self) -> List[TestRun]: + def _init_tests(self) -> list[TestRun]: if self.runner is None: self.runner = self._init_runner() - test_runs: List[TestRun] = [] - test_cases: List[TestCase] = list(self.generate()) + test_runs: list[TestRun] = [] + test_cases: list[TestCase] = list(self.generate()) test_cases += search_dynamic_tests(self) if len(test_cases) == 0: - raise UnexpectedError("No tests found") + msg = "No tests found" + raise UnexpectedError(msg) curr_test: int = 0 test_count = len(test_cases) @@ -110,21 +121,20 @@ def _init_tests(self) -> List[TestRun]: if test_case.attach is None: test_case.attach = self.attach curr_test += 1 - test_runs += [ - TestRun(curr_test, test_count, test_case, self.runner) - ] + test_runs += [TestRun(curr_test, test_count, test_case, self.runner)] return test_runs - def __print_test_num(self, num: int): - total_tests = '' if num == self.curr_test_global else f' ({self.curr_test_global})' + def __print_test_num(self, num: int) -> None: + total_tests = "" if num == self.curr_test_global else f" ({self.curr_test_global})" OutputHandler.get_real_out().write( - RED_BOLD + f'\nStart test {num}{total_tests}' + RESET + '\n' + RED_BOLD + f"\nStart test {num}{total_tests}" + RESET + "\n" ) - def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str]: + def run_tests(self, *, debug=False, is_unittest: bool = False) -> tuple[int, str]: curr_test: int = 0 need_tear_down: bool = False + ex: OutcomeError = None try: if is_tests(self): self.is_tests = True @@ -133,9 +143,10 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str if self.is_tests or debug: import hstest.common.utils as hs - hs.failed_msg_start = '' - hs.failed_msg_continue = '' - hs.success_msg = '' + + hs.failed_msg_start = "" + hs.failed_msg_continue = "" + hs.success_msg = "" SystemHandler.set_up() test_runs = self._init_tests() @@ -153,7 +164,7 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str result: CheckResult = test_run.test() if not result.is_correct: - full_feedback = result.feedback + '\n\n' + test_run.test_case.feedback + full_feedback = result.feedback + "\n\n" + test_run.test_case.feedback raise WrongAnswer(full_feedback.strip()) if test_run.is_last_test(): @@ -163,7 +174,8 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str SystemHandler.tear_down() return passed(is_unittest) - except BaseException as ex: + except BaseException as caught_ex: + ex = caught_ex if need_tear_down: try: StageTest.curr_test_run.tear_down() @@ -171,7 +183,7 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str if isinstance(new_ex, OutcomeError): ex = new_ex - build = 'hs-test-python' + build = "hs-test-python" try: report = build + "\n\n" + get_report() @@ -194,23 +206,21 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str text = get_exception_text(e) except Exception: try: - text = f'{type(e)}: {str(e)}' + text = f"{type(e)}: {e!s}" except Exception: - text = 'Broken exception' + text = "Broken exception" if len(text): traceback += text + "\n\n" - fail_text = 'Unexpected error\n\n' + report + "\n\n" + traceback + fail_text = "Unexpected error\n\n" + report + "\n\n" + traceback except BaseException: # no code execution here allowed so not to throw an exception - fail_text = 'Unexpected error\n\nCannot check the submission\n\n' + report + fail_text = "Unexpected error\n\nCannot check the submission\n\n" + report - try: + with contextlib.suppress(BaseException): SystemHandler.tear_down() - except BaseException: - pass return failed(fail_text, is_unittest) @@ -221,18 +231,19 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str StageTest.source = None self.after_all_tests() - _dynamic_methods: Dict[Type['StageTest'], List[DynamicTestElement]] = {} + _dynamic_methods: dict[type[StageTest], list[DynamicTestElement]] = {} @classmethod - def dynamic_methods(cls) -> List[DynamicTestElement]: + def dynamic_methods(cls) -> list[DynamicTestElement]: if cls in StageTest._dynamic_methods: return StageTest._dynamic_methods[cls] empty = [] StageTest._dynamic_methods[cls] = empty return empty - def generate(self) -> List[TestCase]: + def generate(self) -> list[TestCase]: return [] def check(self, reply: str, attach: Any) -> CheckResult: - raise UnexpectedError('Can\'t check result: override "check" method') + msg = 'Can\'t check result: override "check" method' + raise UnexpectedError(msg) diff --git a/hstest/stage_test.py b/hstest/stage_test.py index fd5cdea0..e3bf4869 100644 --- a/hstest/stage_test.py +++ b/hstest/stage_test.py @@ -1,3 +1,5 @@ # deprecated, but old tests use "from hstest.stage_test import StageTest" # new way to import is "from hstest import StageTest" -from hstest.stage.stage_test import * # noqa: F401, F403 +from __future__ import annotations + +from hstest.stage.stage_test import * # noqa: F403 diff --git a/hstest/test_case/__init__.py b/hstest/test_case/__init__.py index ae866d9d..63b0487e 100644 --- a/hstest/test_case/__init__.py +++ b/hstest/test_case/__init__.py @@ -1,9 +1,11 @@ +from __future__ import annotations + __all__ = [ - 'TestCase', - 'SimpleTestCase', - 'CheckResult', - 'correct', - 'wrong', + "CheckResult", + "SimpleTestCase", + "TestCase", + "correct", + "wrong", ] from hstest.test_case.check_result import CheckResult, correct, wrong diff --git a/hstest/test_case/attach/__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..ad2bf579 100644 --- a/hstest/test_case/attach/django_settings.py +++ b/hstest/test_case/attach/django_settings.py @@ -1,8 +1,8 @@ -from typing import List +from __future__ import annotations class DjangoSettings: port: int = None use_database: bool = False - test_database: str = 'db.test.sqlite3' - tryout_ports: List[int] = [i for i in range(8000, 8101)] + test_database: str = "db.test.sqlite3" + tryout_ports: list[int] = list(range(8000, 8101)) diff --git a/hstest/test_case/attach/flask_settings.py b/hstest/test_case/attach/flask_settings.py index d13cad20..97aa4324 100644 --- a/hstest/test_case/attach/flask_settings.py +++ b/hstest/test_case/attach/flask_settings.py @@ -1,6 +1,6 @@ -from typing import List, Tuple +from __future__ import annotations class FlaskSettings: - sources: List[Tuple[str, int]] = [] - tryout_ports: List[int] = [i for i in range(8000, 8101)] + sources: list[tuple[str, int]] = [] + tryout_ports: list[int] = list(range(8000, 8101)) diff --git a/hstest/test_case/check_result.py b/hstest/test_case/check_result.py index f2e5e1b9..cb291d49 100644 --- a/hstest/test_case/check_result.py +++ b/hstest/test_case/check_result.py @@ -1,11 +1,10 @@ -from typing import Optional +from __future__ import annotations from hstest.exception.outcomes import TestPassed, WrongAnswer class CheckResult: - - def __init__(self, result: bool, feedback: str): + def __init__(self, result: bool, feedback: str) -> None: self._result: bool = result self._feedback: str = feedback @@ -18,21 +17,20 @@ def feedback(self) -> str: return self._feedback @staticmethod - def correct() -> 'CheckResult': - return CheckResult(True, '') + def correct() -> CheckResult: + return CheckResult(True, "") @staticmethod - def wrong(feedback: str) -> 'CheckResult': + def wrong(feedback: str) -> CheckResult: return CheckResult(False, feedback) @staticmethod - def from_error(error: BaseException) -> Optional['CheckResult']: + def from_error(error: BaseException) -> CheckResult | None: if isinstance(error, TestPassed): return correct() - elif isinstance(error, WrongAnswer): + if isinstance(error, WrongAnswer): return wrong(error.feedback) - else: - return None + return None def correct() -> CheckResult: diff --git a/hstest/test_case/test_case.py b/hstest/test_case/test_case.py index 749cc9a9..8a10e852 100644 --- a/hstest/test_case/test_case.py +++ b/hstest/test_case/test_case.py @@ -1,4 +1,6 @@ -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union +from __future__ import annotations + +from typing import Any, TYPE_CHECKING, Union from hstest.dynamic.input.dynamic_input_func import DynamicInputFunction from hstest.dynamic.input.dynamic_testing import to_dynamic_testing @@ -6,49 +8,53 @@ from hstest.test_case.check_result import CheckResult if TYPE_CHECKING: + from collections.abc import Callable + from hstest.dynamic.input.dynamic_input_func import InputFunction from hstest.dynamic.input.dynamic_testing import DynamicTesting SimpleStepikTest = str - AdvancedStepikTest = Tuple[str, Any] + AdvancedStepikTest = tuple[str, Any] StepikTest = Union[SimpleStepikTest, AdvancedStepikTest] CheckFunction = Callable[[str, Any], CheckResult] PredefinedInput = str RuntimeEvaluatedInput = Union[ - PredefinedInput, InputFunction, Tuple[int, InputFunction], DynamicInputFunction] - DynamicInput = Union[PredefinedInput, List[RuntimeEvaluatedInput]] + PredefinedInput, InputFunction, tuple[int, InputFunction], DynamicInputFunction + ] + DynamicInput = Union[PredefinedInput, list[RuntimeEvaluatedInput]] DEFAULT_TIME_LIMIT: int = 15000 class TestCase: def __init__( - self, *, - stdin: 'DynamicInput' = '', - args: List[str] = None, + self, + *, + stdin: DynamicInput = "", + args: list[str] | None = None, attach: Any = None, - feedback: str = '', - files: Dict[str, str] = None, + feedback: str = "", + files: dict[str, str] | None = None, time_limit: int = DEFAULT_TIME_LIMIT, - check_function: 'CheckFunction' = None, - feedback_on_exception: Dict[Type[Exception], str] = None, + check_function: CheckFunction = None, + feedback_on_exception: dict[type[Exception], str] | None = None, copy_to_attach: bool = False, - dynamic_testing: 'DynamicTesting' = None - ): - + dynamic_testing: DynamicTesting = None, + ) -> None: self.source_name = None - self.input: Optional[str] = None - self.args: List[str] = [] if args is None else args + self.input: str | None = None + self.args: list[str] = [] if args is None else args self.attach: Any = attach self.feedback = feedback - self.files: Dict[str, str] = {} if files is None else files + self.files: dict[str, str] = {} if files is None else files self.time_limit: int = time_limit self.check_func: CheckFunction = check_function - self.feedback_on_exception: Dict[Type[Exception], str] = ( - {} if feedback_on_exception is None else feedback_on_exception) + self.feedback_on_exception: dict[type[Exception], str] = ( + {} if feedback_on_exception is None else feedback_on_exception + ) self.input_funcs = [] self._dynamic_testing: DynamicTesting = dynamic_testing @@ -58,13 +64,14 @@ def __init__( if copy_to_attach: if attach is not None: - raise UnexpectedError( - 'Attach is not None ' - 'but copying from stdin is specified') + msg = "Attach is not None " "but copying from stdin is specified" + raise UnexpectedError(msg) if type(stdin) != str: - raise UnexpectedError( - 'To copy stdin to attach stdin should be of type str ' - f'but found type {type(stdin)}') + msg = ( + "To copy stdin to attach stdin should be of type str " + f"but found type {type(stdin)}" + ) + raise UnexpectedError(msg) self.attach = stdin if type(stdin) == str: @@ -72,9 +79,8 @@ def __init__( self.input_funcs = [DynamicInputFunction(1, lambda x: stdin)] else: if type(stdin) != list: - raise UnexpectedError( - 'Stdin should be either of type str or list ' - f'but found type {type(stdin)}') + msg = "Stdin should be either of type str or list " f"but found type {type(stdin)}" + raise UnexpectedError(msg) for elem in stdin: # type: RuntimeEvaluatedInput if type(elem) == DynamicInputFunction: self.input_funcs += [elem] @@ -82,39 +88,45 @@ def __init__( elif type(elem) == str: self.input_funcs += [DynamicInputFunction(1, lambda x, inp=elem: inp)] - elif str(type(elem)) in ["", ""]: + elif str(type(elem)) in {"", ""}: self.input_funcs += [DynamicInputFunction(1, elem)] - elif type(elem) in (tuple, list): + elif type(elem) in {tuple, list}: if len(elem) == 2: trigger_count: int = elem[0] input_function: InputFunction = elem[1] if type(trigger_count) != int: - raise UnexpectedError( - f'Stdin element\'s 1st element should be of type int, ' - f'found {type(trigger_count)}') + msg = ( + f"Stdin element's 1st element should be of type int, " + f"found {type(trigger_count)}" + ) + raise UnexpectedError(msg) if str(type(input_function)) not in { - "", "" + "", + "", }: - raise UnexpectedError( - f'Stdin element\'s 2nd element should be of type function, ' - f'found {type(input_function)}' + msg = ( + f"Stdin element's 2nd element should be of type function, " + f"found {type(input_function)}" ) + raise UnexpectedError(msg) self.input_funcs += [DynamicInputFunction(trigger_count, input_function)] else: - raise UnexpectedError( - f'Stdin element should have size 2, found {len(elem)}') + msg = f"Stdin element should have size 2, found {len(elem)}" + raise UnexpectedError(msg) else: - raise UnexpectedError( - f'Stdin element should have type DynamicInputFunction or ' - f'tuple of size 1 or 2, found element of type {type(elem)}') + msg = ( + f"Stdin element should have type DynamicInputFunction or " + f"tuple of size 1 or 2, found element of type {type(elem)}" + ) + raise UnexpectedError(msg) @property - def dynamic_testing(self) -> 'DynamicTesting': + def dynamic_testing(self) -> DynamicTesting: if self._dynamic_testing is None: self._dynamic_testing = to_dynamic_testing( self.source_name, self.args, self.input_funcs @@ -122,10 +134,10 @@ def dynamic_testing(self) -> 'DynamicTesting': return self._dynamic_testing @staticmethod - def from_stepik(stepik_tests: List['StepikTest']) -> List['TestCase']: + def from_stepik(stepik_tests: list[StepikTest]) -> list[TestCase]: hs_tests = [] for test in stepik_tests: - if type(test) in (list, tuple): + if type(test) in {list, tuple}: hs_test = TestCase(stdin=test[0], attach=test[1]) elif type(test) is str: hs_test = TestCase(stdin=test) @@ -136,10 +148,10 @@ def from_stepik(stepik_tests: List['StepikTest']) -> List['TestCase']: class SimpleTestCase(TestCase): - def __init__(self, *, stdin: str, stdout: str, feedback: str, **kwargs): + def __init__(self, *, stdin: str, stdout: str, feedback: str, **kwargs) -> None: super().__init__(stdin=stdin, attach=stdout, feedback=feedback, **kwargs) self.check_func = self._custom_check def _custom_check(self, reply: str, expected: str): is_correct = reply.strip() == expected.strip() - return CheckResult(is_correct, '') + return CheckResult(is_correct, "") diff --git a/hstest/testing/__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..e002b364 100644 --- a/hstest/testing/execution/filtering/file_filter.py +++ b/hstest/testing/execution/filtering/file_filter.py @@ -1,39 +1,47 @@ +from __future__ import annotations + import re -from typing import Callable, Dict, Set +from collections.abc import Callable Folder = str File = str Source = str Module = str -Sources = Dict[File, Source] +Sources = dict[File, Source] Filter = Callable[[Folder, File, Source], bool] -no_filter: Filter = lambda *a, **kw: True + +def no_filter(*a, **kw) -> bool: + return True class FileFilter: - def __init__(self, - init_files: Callable[[Folder, Sources], None] = no_filter, - folder: Callable[[Folder], bool] = no_filter, - file: Callable[[File], bool] = no_filter, - source: Callable[[Source], bool] = no_filter, - generic: Filter = no_filter): + def __init__( + self, + init_files: Callable[[Folder, Sources], None] = no_filter, + folder: Callable[[Folder], bool] = no_filter, + file: Callable[[File], bool] = no_filter, + source: Callable[[Source], bool] = no_filter, + generic: Filter = no_filter, + ) -> None: self.init_files = init_files self.folder = folder self.file = file self.source = source self.generic = generic - self.filtered: Set[File] = set() + self.filtered: set[File] = set() @staticmethod def regex_filter(regex: str): - return lambda s: re.compile(regex, re.M).search(s) is not None + return lambda s: re.compile(regex, re.MULTILINE).search(s) is not None - def init_filter(self, folder: Folder, sources: Sources): + def init_filter(self, folder: Folder, sources: Sources) -> None: self.init_files(folder, sources) def filter(self, folder: Folder, file: File, source: Source) -> bool: - return self.folder(folder) \ - and self.file(file) \ - and self.source(source) \ + return ( + self.folder(folder) + and self.file(file) + and self.source(source) and self.generic(folder, file, source) + ) diff --git a/hstest/testing/execution/filtering/main_filter.py b/hstest/testing/execution/filtering/main_filter.py index 53ff76bb..2a9dbcf3 100644 --- a/hstest/testing/execution/filtering/main_filter.py +++ b/hstest/testing/execution/filtering/main_filter.py @@ -1,17 +1,30 @@ -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from hstest.testing.execution.filtering.file_filter import ( - File, FileFilter, Filter, Folder, no_filter, Source, Sources + File, + FileFilter, + Filter, + Folder, + no_filter, + Source, + Sources, ) +if TYPE_CHECKING: + from collections.abc import Callable + class MainFilter(FileFilter): - def __init__(self, - program_should_contain: str, - init_files: Callable[[Folder, Sources], None] = no_filter, - folder: Callable[[Folder], bool] = no_filter, - file: Callable[[File], bool] = no_filter, - source: Callable[[Source], bool] = no_filter, - generic: Filter = no_filter): + def __init__( + self, + program_should_contain: str, + init_files: Callable[[Folder, Sources], None] = no_filter, + folder: Callable[[Folder], bool] = no_filter, + file: Callable[[File], bool] = no_filter, + source: Callable[[Source], bool] = no_filter, + generic: Filter = no_filter, + ) -> None: super().__init__(init_files, folder, file, source, generic) self.program_should_contain = program_should_contain diff --git a/hstest/testing/execution/main_module_executor.py b/hstest/testing/execution/main_module_executor.py index e319317e..363c668f 100644 --- a/hstest/testing/execution/main_module_executor.py +++ b/hstest/testing/execution/main_module_executor.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import os import runpy import sys -from concurrent.futures import Future -from typing import Optional +from typing import TYPE_CHECKING from hstest.common.process_utils import DaemonThreadPoolExecutor from hstest.dynamic.output.output_handler import OutputHandler @@ -13,30 +14,30 @@ from hstest.testing.execution.program_executor import ProgramExecutor, ProgramState from hstest.testing.execution.searcher.python_searcher import PythonSearcher +if TYPE_CHECKING: + from concurrent.futures import Future + class MainModuleExecutor(ProgramExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__() - OutputHandler.print(f'MainModuleExecutor instantiating, source = {source_name}') + OutputHandler.print(f"MainModuleExecutor instantiating, source = {source_name}") self.runnable = PythonSearcher().find(source_name) - self.__executor: Optional[DaemonThreadPoolExecutor] = None - self.__task: Optional[Future] = None + self.__executor: DaemonThreadPoolExecutor | None = None + self.__task: Future | None = None self.__group = None self.working_directory_before = os.path.abspath(os.getcwd()) - def _invoke_method(self, *args: str): + def _invoke_method(self, *args: str) -> None: from hstest.stage_test import StageTest try: self._machine.set_state(ProgramState.RUNNING) - sys.argv = [self.runnable.file] + list(args) + sys.argv = [self.runnable.file, *list(args)] sys.path.insert(0, self.runnable.folder) - runpy.run_module( - self.runnable.module, - run_name="__main__" - ) + runpy.run_module(self.runnable.module, run_name="__main__") self._machine.set_state(ProgramState.FINISHED) @@ -48,30 +49,30 @@ def _invoke_method(self, *args: str): self._machine.set_state(ProgramState.FINISHED) return - StageTest.curr_test_run.set_error_in_test(ExceptionWithFeedback('', ex)) + StageTest.curr_test_run.set_error_in_test(ExceptionWithFeedback("", ex)) self._machine.set_state(ProgramState.EXCEPTION_THROWN) - def _launch(self, *args: str): - self.modules_before = [k for k in sys.modules.keys()] + def _launch(self, *args: str) -> None: + self.modules_before = list(sys.modules.keys()) from hstest.stage_test import StageTest + test_num = StageTest.curr_test_run.test_num self.__group = ThreadGroup() SystemHandler.install_handler( - self, - lambda: ThreadGroup.curr_group() == self.__group, - lambda: self.request_input() + self, lambda: ThreadGroup.curr_group() == self.__group, self.request_input ) self.__executor = DaemonThreadPoolExecutor( - name=f"MainModuleExecutor test #{test_num}", group=self.__group) + name=f"MainModuleExecutor test #{test_num}", group=self.__group + ) self.__task = self.__executor.submit(lambda: self._invoke_method(*args)) - def _terminate(self): + def _terminate(self) -> None: self.__executor.shutdown(wait=False) self.__task.cancel() with self._machine.cv: diff --git a/hstest/testing/execution/process/__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..9e4a3ac7 100644 --- a/hstest/testing/execution/process/cpp_executor.py +++ b/hstest/testing/execution/process/cpp_executor.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from hstest.common.os_utils import is_windows @@ -6,27 +8,36 @@ class CppExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(CppSearcher().find(source_name)) self.without_extension = os.path.splitext(self.runnable.file)[0] if is_windows(): self.executable = self.without_extension - self.file_name = self.executable + '.exe' + self.file_name = self.executable + ".exe" else: - self.executable = f'./{self.without_extension}' + self.executable = f"./{self.without_extension}" self.file_name = self.without_extension def _compilation_command(self): - return ['g++', '-std=c++20', '-pipe', '-O2', '-static', '-o', self.file_name, self.runnable.file] + return [ + "g++", + "-std=c++20", + "-pipe", + "-O2", + "-static", + "-o", + self.file_name, + self.runnable.file, + ] def _filter_compilation_error(self, error: str) -> str: return error def _execution_command(self, *args: str): - return [self.executable] + list(args) + return [self.executable, *list(args)] - def _cleanup(self): + def _cleanup(self) -> None: if os.path.exists(self.file_name): os.remove(self.file_name) diff --git a/hstest/testing/execution/process/go_executor.py b/hstest/testing/execution/process/go_executor.py index 057ca243..e5d202dc 100644 --- a/hstest/testing/execution/process/go_executor.py +++ b/hstest/testing/execution/process/go_executor.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from hstest.common.os_utils import is_windows @@ -6,28 +8,28 @@ class GoExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(GoSearcher().find(source_name)) - self.without_go = self.runnable.file[:-len(GoSearcher().extension)] + self.without_go = self.runnable.file[: -len(GoSearcher().extension)] if is_windows(): self.executable = self.without_go - self.file_name = self.executable + '.exe' + self.file_name = self.executable + ".exe" else: - self.executable = f'./{self.without_go}' + self.executable = f"./{self.without_go}" self.file_name = self.without_go def _compilation_command(self): - return ['go', 'build', self.runnable.file] + return ["go", "build", self.runnable.file] def _filter_compilation_error(self, error: str) -> str: - error_lines = [line for line in error.splitlines() if not line.startswith('#')] - return '\n'.join(error_lines) + error_lines = [line for line in error.splitlines() if not line.startswith("#")] + return "\n".join(error_lines) def _execution_command(self, *args: str): - return [self.executable] + list(args) + return [self.executable, *list(args)] - def _cleanup(self): + def _cleanup(self) -> None: if os.path.exists(self.file_name): os.remove(self.file_name) diff --git a/hstest/testing/execution/process/javascript_executor.py b/hstest/testing/execution/process/javascript_executor.py index 8ec4dbaf..c90d3258 100644 --- a/hstest/testing/execution/process/javascript_executor.py +++ b/hstest/testing/execution/process/javascript_executor.py @@ -1,10 +1,12 @@ +from __future__ import annotations + from hstest.testing.execution.process_executor import ProcessExecutor from hstest.testing.execution.searcher.javascript_searcher import JavascriptSearcher class JavascriptExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(JavascriptSearcher().find(source_name)) def _execution_command(self, *args: str): - return ['node', self.runnable.file] + list(args) + return ["node", self.runnable.file, *list(args)] diff --git a/hstest/testing/execution/process/python_executor.py b/hstest/testing/execution/process/python_executor.py index 23d032e9..32d3721d 100644 --- a/hstest/testing/execution/process/python_executor.py +++ b/hstest/testing/execution/process/python_executor.py @@ -1,10 +1,23 @@ +from __future__ import annotations + +import os + +from hstest.common.os_utils import is_windows from hstest.testing.execution.process_executor import ProcessExecutor from hstest.testing.execution.searcher.python_searcher import PythonSearcher class PythonExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(PythonSearcher().find(source_name)) + # Set UTF-8 encoding for Python I/O on Windows + if is_windows(): + os.environ["PYTHONIOENCODING"] = "utf8" def _execution_command(self, *args: str): - return ['python', '-u', self.runnable.file] + list(args) + cmd = ["python"] + if is_windows(): # Works on all Windows versions (32/64 bit) + # Set UTF-8 encoding for stdin/stdout on Windows + cmd.extend(["-X", "utf8"]) + cmd.extend(["-u", self.runnable.file, *list(args)]) + return cmd diff --git a/hstest/testing/execution/process/shell_executor.py b/hstest/testing/execution/process/shell_executor.py index 71387ea5..2868866b 100644 --- a/hstest/testing/execution/process/shell_executor.py +++ b/hstest/testing/execution/process/shell_executor.py @@ -1,10 +1,12 @@ +from __future__ import annotations + from hstest.testing.execution.process_executor import ProcessExecutor from hstest.testing.execution.searcher.shell_searcher import ShellSearcher class ShellExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(ShellSearcher().find(source_name)) def _execution_command(self, *args: str): - return ['bash', self.runnable.file] + list(args) + return ["bash", self.runnable.file, *list(args)] diff --git a/hstest/testing/execution/process_executor.py b/hstest/testing/execution/process_executor.py index 7e0bf46e..62391259 100644 --- a/hstest/testing/execution/process_executor.py +++ b/hstest/testing/execution/process_executor.py @@ -1,7 +1,10 @@ +from __future__ import annotations + +import contextlib import os from threading import Thread from time import sleep -from typing import List, Optional +from typing import TYPE_CHECKING from hstest.common.utils import try_many_times from hstest.dynamic.input.input_handler import InputHandler @@ -11,32 +14,35 @@ from hstest.dynamic.system_handler import SystemHandler from hstest.exception.outcomes import CompilationError, ExceptionWithFeedback, OutOfInputError from hstest.testing.execution.program_executor import ProgramExecutor, ProgramState -from hstest.testing.execution.runnable.runnable_file import RunnableFile from hstest.testing.process_wrapper import ProcessWrapper +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile + class ProcessExecutor(ProgramExecutor): compiled = False - def __init__(self, runnable: RunnableFile): + def __init__(self, runnable: RunnableFile) -> None: super().__init__() - self.process: Optional[ProcessWrapper] = None + self.process: ProcessWrapper | None = None self.thread = None self.continue_executing = True self.runnable: RunnableFile = runnable - self.__group: Optional[ThreadGroup] = None + self.__group: ThreadGroup | None = None self.working_directory_before = os.path.abspath(os.getcwd()) - def _compilation_command(self, *args: str) -> List[str]: + def _compilation_command(self, *args: str) -> list[str]: return [] def _filter_compilation_error(self, error: str) -> str: return error - def _execution_command(self, *args: str) -> List[str]: - raise NotImplementedError('Method "_execution_command" isn\'t implemented') + def _execution_command(self, *args: str) -> list[str]: + msg = 'Method "_execution_command" isn\'t implemented' + raise NotImplementedError(msg) - def _cleanup(self): + def _cleanup(self) -> None: pass def __compile_program(self) -> bool: @@ -55,13 +61,14 @@ def __compile_program(self) -> bool: error_text = self._filter_compilation_error(process.stderr) from hstest import StageTest + StageTest.curr_test_run.set_error_in_test(CompilationError(error_text)) self._machine.set_state(ProgramState.COMPILATION_ERROR) return False return True - def __handle_process(self, *args: str): + def __handle_process(self, *args: str) -> None: from hstest import StageTest os.chdir(self.runnable.folder) @@ -77,109 +84,102 @@ def __handle_process(self, *args: str): self.process = ProcessWrapper(*command).start() while self.continue_executing: - OutputHandler.print('Handle process - one iteration') + OutputHandler.print("Handle process - one iteration") sleep(0.001) if self.process.is_finished(): - OutputHandler.print('Handle process - finished, breaking') + OutputHandler.print("Handle process - finished, breaking") break is_input_allowed = self.is_input_allowed() is_waiting_input = self.process.is_waiting_input() - OutputHandler.print(f'Handle process - ' - f'input allowed {is_input_allowed}, ' - f'waiting input {is_waiting_input}') + OutputHandler.print( + f"Handle process - " + f"input allowed {is_input_allowed}, " + f"waiting input {is_waiting_input}" + ) if is_input_allowed and is_waiting_input: - OutputHandler.print('Handle process - registering input request') + OutputHandler.print("Handle process - registering input request") self.process.register_input_request() try: - OutputHandler.print('Handle process - try readline') + OutputHandler.print("Handle process - try readline") next_input = InputHandler.mock_in.readline() - OutputHandler.print( - f'Handle process - requested input: {repr(next_input)}' - ) + OutputHandler.print(f"Handle process - requested input: {next_input!r}") self.process.provide_input(next_input) - OutputHandler.print( - f'Handle process - written to stdin: {repr(next_input)}' - ) + OutputHandler.print(f"Handle process - written to stdin: {next_input!r}") except ExitException: - OutputHandler.print('Handle process - EXIT EXCEPTION, stop input') + OutputHandler.print("Handle process - EXIT EXCEPTION, stop input") if self._wait_if_terminated(): if type(StageTest.curr_test_run.error_in_test) == OutOfInputError: StageTest.curr_test_run.set_error_in_test(None) OutputHandler.print( - 'Handle process - Abort stopping input, everything is OK' + "Handle process - Abort stopping input, everything is OK" ) break self.stop_input() except BaseException as ex: - OutputHandler.print(f'Handle process - SOME EXCEPTION {ex}') + OutputHandler.print(f"Handle process - SOME EXCEPTION {ex}") - OutputHandler.print('Handle process - TERMINATE') + OutputHandler.print("Handle process - TERMINATE") self.process.terminate() is_error_happened = self.process.is_error_happened() - OutputHandler.print('Handle process - after termination') - OutputHandler.print(f'Handle process - is error happened {is_error_happened}') + OutputHandler.print("Handle process - after termination") + OutputHandler.print(f"Handle process - is error happened {is_error_happened}") if StageTest.curr_test_run.error_in_test is not None: - OutputHandler.print('Handle process - set state EXCEPTION THROWN (ERROR IN TEST)') + OutputHandler.print("Handle process - set state EXCEPTION THROWN (ERROR IN TEST)") self._machine.set_state(ProgramState.EXCEPTION_THROWN) elif is_error_happened: - OutputHandler.print( - 'Handle process - set state EXCEPTION THROWN (REALLY EXCEPTION)' - ) + OutputHandler.print("Handle process - set state EXCEPTION THROWN (REALLY EXCEPTION)") StageTest.curr_test_run.set_error_in_test( ExceptionWithFeedback(self.process.stderr, None) ) self._machine.set_state(ProgramState.EXCEPTION_THROWN) else: - OutputHandler.print('Handle process - set state FINISHED') + OutputHandler.print("Handle process - set state FINISHED") self._machine.set_state(ProgramState.FINISHED) - OutputHandler.print('Handle process - finishing execution') + OutputHandler.print("Handle process - finishing execution") def _wait_if_terminated(self): return try_many_times(100, 10, lambda: self.process.is_finished(False)) - def _launch(self, *args: str): + def _launch(self, *args: str) -> None: self.__group = ThreadGroup() SystemHandler.install_handler( - self, - lambda: ThreadGroup.curr_group() == self.__group, - lambda: self.request_input() + self, lambda: ThreadGroup.curr_group() == self.__group, self.request_input ) - self.thread = Thread(target=lambda: self.__handle_process(*args), daemon=True, - group=self.__group) + self.thread = Thread( + target=lambda: self.__handle_process(*args), daemon=True, group=self.__group + ) self.thread.start() - def _terminate(self): + def _terminate(self) -> None: self.continue_executing = False self.process.terminate() - OutputHandler.print(f'TERMINATE {self.is_finished()}') + OutputHandler.print(f"TERMINATE {self.is_finished()}") os.chdir(self.working_directory_before) while not self.is_finished(): if self.is_waiting_input(): self._machine.set_state(ProgramState.RUNNING) - OutputHandler.print(f'NOT FINISHED {self._machine.state}') + OutputHandler.print(f"NOT FINISHED {self._machine.state}") sleep(0.001) - def tear_down(self): + def tear_down(self) -> None: working_directory_before = os.path.abspath(os.getcwd()) os.chdir(self.runnable.folder) - try: + with contextlib.suppress(BaseException): self._cleanup() - except BaseException: - pass ProcessExecutor.compiled = False os.chdir(working_directory_before) diff --git a/hstest/testing/execution/program_executor.py b/hstest/testing/execution/program_executor.py index a1b0dc80..2ad71f20 100644 --- a/hstest/testing/execution/program_executor.py +++ b/hstest/testing/execution/program_executor.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from enum import Enum -from typing import Optional +from typing import NoReturn from hstest.dynamic.output.output_handler import OutputHandler from hstest.exception.outcomes import ErrorWithFeedback, UnexpectedError @@ -17,8 +19,8 @@ class ProgramState(Enum): class ProgramExecutor: - def __init__(self, source_name: str = None): - self._input: Optional[str] = None + def __init__(self, source_name: str | None = None) -> None: + self._input: str | None = None self.__in_background: bool = False self.__no_more_input: bool = False @@ -33,18 +35,21 @@ def __init__(self, source_name: str = None): m.add_transition(ProgramState.RUNNING, ProgramState.EXCEPTION_THROWN) m.add_transition(ProgramState.RUNNING, ProgramState.FINISHED) - def _launch(self, *args: str): - raise NotImplementedError('Method "_launch" isn\'t implemented') + def _launch(self, *args: str) -> NoReturn: + msg = 'Method "_launch" isn\'t implemented' + raise NotImplementedError(msg) - def _terminate(self): - raise NotImplementedError('Method "_terminate" isn\'t implemented') + def _terminate(self) -> NoReturn: + msg = 'Method "_terminate" isn\'t implemented' + raise NotImplementedError(msg) def get_output(self) -> str: return OutputHandler.get_partial_output(self) def start(self, *args: str) -> str: if not self._machine.in_state(ProgramState.NOT_STARTED): - raise UnexpectedError(f"Cannot start the program {self} twice") + msg = f"Cannot start the program {self} twice" + raise UnexpectedError(msg) self._launch(*args) @@ -52,23 +57,23 @@ def start(self, *args: str) -> str: self._machine.wait_not_state(ProgramState.NOT_STARTED) return "" - self._machine.wait_not_states( - ProgramState.NOT_STARTED, ProgramState.RUNNING) + self._machine.wait_not_states(ProgramState.NOT_STARTED, ProgramState.RUNNING) - OutputHandler.print('Program executor - after waiting in start() method') + OutputHandler.print("Program executor - after waiting in start() method") return self.__get_execution_output() def execute(self, stdin: str) -> str: if self.is_finished(): from hstest.stage_test import StageTest + StageTest.curr_test_run.set_error_in_test( ErrorWithFeedback( - f"The program {self} has unexpectedly terminated.\n" + - "It finished execution too early, should continue running." + f"The program {self} has unexpectedly terminated.\n" + + "It finished execution too early, should continue running." ) ) - raise TestedProgramFinishedEarly() + raise TestedProgramFinishedEarly if stdin is None: self.stop_input() @@ -76,12 +81,13 @@ def execute(self, stdin: str) -> str: if not self.is_waiting_input(): raise UnexpectedError( - f"Program {self} is not waiting for the input " + - f"(state == \"{self._machine.state}\")") + f"Program {self} is not waiting for the input " + + f'(state == "{self._machine.state}")' + ) if self.__no_more_input: - raise UnexpectedError( - f"Can't pass input to the program {self} - input was prohibited.") + msg = f"Can't pass input to the program {self} - input was prohibited." + raise UnexpectedError(msg) self._input = stdin if self.__in_background: @@ -93,29 +99,29 @@ def execute(self, stdin: str) -> str: self._machine.set_and_wait(ProgramState.RUNNING) return self.__get_execution_output() - def stop(self): + def stop(self) -> None: self.__no_more_input = True self._terminate() def __get_execution_output(self) -> str: - OutputHandler.print('Program executor - __get_execution_output()') + OutputHandler.print("Program executor - __get_execution_output()") if self._machine.in_state(ProgramState.EXCEPTION_THROWN): - raise TestedProgramThrewException() - OutputHandler.print('Program executor - __get_execution_output() NO EXCEPTION') + raise TestedProgramThrewException + OutputHandler.print("Program executor - __get_execution_output() NO EXCEPTION") if self.__return_output_after_execution: return self.get_output() return "" - def request_input(self) -> Optional[str]: + def request_input(self) -> str | None: if self.__no_more_input: return None - OutputHandler.print('Program executor - _request_input() invoked, set state WAITING') + OutputHandler.print("Program executor - _request_input() invoked, set state WAITING") self._machine.set_and_wait(ProgramState.WAITING, ProgramState.RUNNING) input_local = self._input self._input = None return input_local - def set_return_output_after_execution(self, value: bool): + def set_return_output_after_execution(self, value: bool) -> None: self.__return_output_after_execution = value def is_finished(self) -> bool: @@ -123,7 +129,7 @@ def is_finished(self) -> bool: exception = self._machine.in_state(ProgramState.EXCEPTION_THROWN) return finished or exception - def stop_input(self): + def stop_input(self) -> None: self.__in_background = True self.__no_more_input = True if self.is_waiting_input(): @@ -135,22 +141,23 @@ def is_input_allowed(self) -> bool: def is_waiting_input(self) -> bool: return self._machine.in_state(ProgramState.WAITING) - def start_in_background(self, *args: str): + def start_in_background(self, *args: str) -> None: self.__in_background = True self.start(*args) - def go_background(self): + def go_background(self) -> None: self.__in_background = True - def stop_background(self): + def stop_background(self) -> None: self.__in_background = False self._machine.wait_state(ProgramState.WAITING) def is_in_background(self): return self.__in_background - def tear_down(self): + def tear_down(self) -> None: pass def __str__(self) -> str: - raise NotImplementedError('Method "__str__" isn\'t implemented') + msg = 'Method "__str__" isn\'t implemented' + raise NotImplementedError(msg) diff --git a/hstest/testing/execution/runnable/__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..5c90d645 100644 --- a/hstest/testing/execution/runnable/runnable_file.py +++ b/hstest/testing/execution/runnable/runnable_file.py @@ -1,7 +1,12 @@ -from hstest.testing.execution.filtering.file_filter import File, Folder +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from hstest.testing.execution.filtering.file_filter import File, Folder class RunnableFile: - def __init__(self, folder: Folder, file: File): + def __init__(self, folder: Folder, file: File) -> None: self.folder = folder self.file = file diff --git a/hstest/testing/execution/searcher/__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..a72e96c5 100644 --- a/hstest/testing/execution/searcher/base_searcher.py +++ b/hstest/testing/execution/searcher/base_searcher.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import os import re -from typing import Dict, List, Optional, Set, Tuple, Union from hstest.common.file_utils import walk_user_files from hstest.exception.outcomes import ErrorWithFeedback, UnexpectedError @@ -15,13 +16,15 @@ class BaseSearcher: @property def extension(self) -> str: - raise NotImplementedError('Property "extension" should be implemented') + msg = 'Property "extension" should be implemented' + raise NotImplementedError(msg) - def search(self, where_to_search: str = None) -> RunnableFile: - raise NotImplementedError('Method "search" should be implemented') + def search(self, where_to_search: str | None = None) -> RunnableFile: + msg = 'Method "search" should be implemented' + raise NotImplementedError(msg) @staticmethod - def _get_contents(folder: Folder, files: List[File]) -> Dict[File, Source]: + def _get_contents(folder: Folder, files: list[File]) -> dict[File, Source]: contents = {} for file in files: @@ -29,7 +32,7 @@ def _get_contents(folder: Folder, files: List[File]) -> Dict[File, Source]: if path in file_contents_cached: contents[file] = file_contents_cached[path] elif os.path.exists(path): - with open(path) as f: + with open(path, encoding="utf-8") as f: try: file_content = f.read() except UnicodeDecodeError: @@ -48,21 +51,18 @@ def _search_non_cached( pre_main_filter: FileFilter, main_filter: MainFilter, post_main_filter: FileFilter, - force_content_filters: Union[List[MainFilter], None] = None + force_content_filters: list[MainFilter] | None = None, ) -> RunnableFile: - if not force_content_filters: force_content_filters = [] curr_folder = os.path.abspath(where_to_search) - for folder, dirs, files in walk_user_files(curr_folder): - + for folder, _dirs, files in walk_user_files(curr_folder): contents = self._get_contents(folder, files) initial_filter = FileFilter( - file=lambda f: f.endswith(self.extension), - generic=file_filter.filter + file=lambda f: f.endswith(self.extension), generic=file_filter.filter ) candidates = set(files) @@ -70,8 +70,9 @@ def _search_non_cached( for curr_filter in initial_filter, pre_main_filter, main_filter, post_main_filter: curr_filter.init_filter(folder, contents) - filtered_files: Set[File] = { - file for file in files + filtered_files: set[File] = { + file + for file in files if file in contents and curr_filter.filter(folder, file, contents[file]) } @@ -80,15 +81,14 @@ def _search_non_cached( if len(filtered_files) == 0: if curr_filter == initial_filter: break - else: - continue - elif curr_filter == initial_filter: + continue + if curr_filter == initial_filter: for forced_filter in force_content_filters: filtered_files = { - file for file in filtered_files - if file in contents and forced_filter.filter( - folder, file, contents[file] - ) + file + for file in filtered_files + if file in contents + and forced_filter.filter(folder, file, contents[file]) } if len(filtered_files) == 0: should_contain = [ @@ -96,9 +96,8 @@ def _search_non_cached( for forced_filter in force_content_filters if isinstance(forced_filter, MainFilter) ] - raise ErrorWithFeedback( - f'The runnable file should contain all the following lines: {should_contain}' - ) + msg = f"The runnable file should contain all the following lines: {should_contain}" + raise ErrorWithFeedback(msg) if len(filtered_files) == 1: file = filtered_files.pop() @@ -116,46 +115,55 @@ def _search_non_cached( continue if len(candidates) > 1 and len(main_filter.filtered) > 0: - str_files = ', '.join(f'"{f}"' for f in sorted(candidates)) + str_files = ", ".join(f'"{f}"' for f in sorted(candidates)) all_have = [] if main_filter.program_should_contain: all_have.append(main_filter.program_should_contain) - all_have.extend([ - forced_filter.program_should_contain for forced_filter in force_content_filters - if isinstance(forced_filter, MainFilter) - ]) - raise ErrorWithFeedback( - f'Cannot decide which file to run out of the following: {str_files}\n' - f'They all have {all_have}. ' - f'Leave one file with this lines.') + all_have.extend( + [ + forced_filter.program_should_contain + for forced_filter in force_content_filters + if isinstance(forced_filter, MainFilter) + ] + ) + msg = ( + f"Cannot decide which file to run out of the following: {str_files}\n" + f"They all have {all_have}. " + f"Leave one file with this lines." + ) + raise ErrorWithFeedback(msg) if len(candidates) == 0: candidates = initial_filter.filtered - str_files = ', '.join(f'"{f}"' for f in sorted(candidates)) + str_files = ", ".join(f'"{f}"' for f in sorted(candidates)) - raise ErrorWithFeedback( - f'Cannot decide which file to run out of the following: {str_files}\n' + msg = ( + f"Cannot decide which file to run out of the following: {str_files}\n" f'Write "{main_filter.program_should_contain}" ' - f'in one of them to mark it as an entry point.') + f"in one of them to mark it as an entry point." + ) + raise ErrorWithFeedback(msg) - raise ErrorWithFeedback( - 'Cannot find a file to execute your code.\n' - f'Are your project files located at \"{curr_folder}\"?') + msg = ( + "Cannot find a file to execute your code.\n" + f'Are your project files located at "{curr_folder}"?' + ) + raise ErrorWithFeedback(msg) def _search( self, - where_to_search: str = None, + where_to_search: str | None = None, *, file_filter: FileFilter = None, pre_main_filter: FileFilter = None, main_filter: MainFilter = None, post_main_filter: FileFilter = None, - force_content_filters: Union[List[MainFilter], None] = None + force_content_filters: list[MainFilter] | None = None, ) -> RunnableFile: - - if not self.extension.startswith('.'): - raise UnexpectedError(f'File extension "{self.extension}" should start with a dot') + if not self.extension.startswith("."): + msg = f'File extension "{self.extension}" should start with a dot' + raise UnexpectedError(msg) if where_to_search is None: where_to_search = os.getcwd() @@ -193,27 +201,25 @@ def _search( return result - def _simple_search(self, - where_to_search: str, - main_desc: str, - main_regex: str, - force_content_filters: Union[List[MainFilter], None] = None - ) -> RunnableFile: - main_searcher = re.compile(main_regex, re.M) + def _simple_search( + self, + where_to_search: str, + main_desc: str, + main_regex: str, + force_content_filters: list[MainFilter] | None = None, + ) -> RunnableFile: + main_searcher = re.compile(main_regex, re.MULTILINE) return self._search( where_to_search, - main_filter=MainFilter( - main_desc, - source=lambda s: main_searcher.search(s) is not None - ), - force_content_filters=force_content_filters + main_filter=MainFilter(main_desc, source=lambda s: main_searcher.search(s) is not None), + force_content_filters=force_content_filters, ) def _base_search(self, where_to_search: str) -> RunnableFile: - return self._simple_search(where_to_search, main_desc='', main_regex='') + return self._simple_search(where_to_search, main_desc="", main_regex="") - def find(self, source: Optional[str]) -> RunnableFile: - if source in [None, '']: + def find(self, source: str | None) -> RunnableFile: + if source in {None, ""}: return self.search() ext = self.extension @@ -223,38 +229,38 @@ def find(self, source: Optional[str]) -> RunnableFile: if source_folder is not None and os.path.isdir(source_folder): return self.search(source_folder) - elif source_file is not None and os.path.isfile(source_file): - path, sep, file = source_module.rpartition('.') - folder = os.path.abspath(path.replace('.', os.sep)) + if source_file is not None and os.path.isfile(source_file): + path, _sep, file = source_module.rpartition(".") + folder = os.path.abspath(path.replace(".", os.sep)) return RunnableFile(folder, file + ext) - else: - path, _, _ = source_module.rpartition('.') - folder = os.path.abspath(path.replace('.', os.sep)) - raise ErrorWithFeedback( - 'Cannot find a file to execute your code.\n' - f'Are your project files located at \"{folder}\"?') + path, _, _ = source_module.rpartition(".") + folder = os.path.abspath(path.replace(".", os.sep)) + msg = ( + "Cannot find a file to execute your code.\n" + f'Are your project files located at "{folder}"?' + ) + raise ErrorWithFeedback(msg) - def _parse_source(self, source: str) -> Tuple[Folder, File, Module]: + def _parse_source(self, source: str) -> tuple[Folder, File, Module]: ext = self.extension - source = source.replace('/', os.sep).replace('\\', os.sep) + source = source.replace("/", os.sep).replace("\\", os.sep) if source.endswith(ext): source_folder = None source_file = source - source_module = source[:-len(ext)].replace(os.sep, '.') + source_module = source[: -len(ext)].replace(os.sep, ".") elif os.sep in source: - if source.endswith(os.sep): - source = source[:-len(os.sep)] + source = source.removesuffix(os.sep) source_folder = source source_file = None - source_module = source.replace(os.sep, '.') + source_module = source.replace(os.sep, ".") else: - source_folder = source.replace('.', os.sep) + source_folder = source.replace(".", os.sep) source_file = source_folder + ext source_module = source diff --git a/hstest/testing/execution/searcher/cpp_searcher.py b/hstest/testing/execution/searcher/cpp_searcher.py index b01fac3a..c0173070 100644 --- a/hstest/testing/execution/searcher/cpp_searcher.py +++ b/hstest/testing/execution/searcher/cpp_searcher.py @@ -1,25 +1,26 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING from hstest.testing.execution.filtering.main_filter import MainFilter -from hstest.testing.execution.runnable.runnable_file import RunnableFile from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class CppSearcher(BaseSearcher): +class CppSearcher(BaseSearcher): @property def extension(self) -> str: - return '.cpp' + return ".cpp" - def search(self, where: str = None) -> RunnableFile: - main_func_searcher = re.compile(r'(^|\n)\s*int\s+main\s*\(.*\)', re.M) + def search(self, where: str | None = None) -> RunnableFile: + main_func_searcher = re.compile(r"(^|\n)\s*int\s+main\s*\(.*\)", re.MULTILINE) return self._search( where, force_content_filters=[ - MainFilter( - 'int main()', - source=lambda s: main_func_searcher.search(s) is not None - ), - ] + MainFilter("int main()", source=lambda s: main_func_searcher.search(s) is not None), + ], ) diff --git a/hstest/testing/execution/searcher/go_searcher.py b/hstest/testing/execution/searcher/go_searcher.py index aa00bbe2..838dfc5d 100644 --- a/hstest/testing/execution/searcher/go_searcher.py +++ b/hstest/testing/execution/searcher/go_searcher.py @@ -1,30 +1,30 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING from hstest.testing.execution.filtering.main_filter import MainFilter -from hstest.testing.execution.runnable.runnable_file import RunnableFile from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class GoSearcher(BaseSearcher): +class GoSearcher(BaseSearcher): @property def extension(self) -> str: - return '.go' + return ".go" - def search(self, where: str = None) -> RunnableFile: - package_searcher = re.compile(r'^\s*package\s*main', re.M) - main_func_searcher = re.compile(r'(^|\n)\s*func\s+main\s*\(\s*\)', re.M) + def search(self, where: str | None = None) -> RunnableFile: + package_searcher = re.compile(r"^\s*package\s*main", re.MULTILINE) + main_func_searcher = re.compile(r"(^|\n)\s*func\s+main\s*\(\s*\)", re.MULTILINE) return self._search( where, force_content_filters=[ + MainFilter("package main", source=lambda s: package_searcher.search(s) is not None), MainFilter( - 'package main', - source=lambda s: package_searcher.search(s) is not None - ), - MainFilter( - 'func main()', - source=lambda s: main_func_searcher.search(s) is not None + "func main()", source=lambda s: main_func_searcher.search(s) is not None ), - ] + ], ) diff --git a/hstest/testing/execution/searcher/javascript_searcher.py b/hstest/testing/execution/searcher/javascript_searcher.py index 027ea5a9..f1ec811f 100644 --- a/hstest/testing/execution/searcher/javascript_searcher.py +++ b/hstest/testing/execution/searcher/javascript_searcher.py @@ -1,12 +1,17 @@ -from hstest.testing.execution.runnable.runnable_file import RunnableFile +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class JavascriptSearcher(BaseSearcher): +class JavascriptSearcher(BaseSearcher): @property def extension(self) -> str: - return '.js' + return ".js" - def search(self, where: str = None) -> RunnableFile: - return self._simple_search(where, "function main()", r'(^|\n) *function +main +\( *\)') + def search(self, where: str | None = None) -> RunnableFile: + return self._simple_search(where, "function main()", r"(^|\n) *function +main +\( *\)") diff --git a/hstest/testing/execution/searcher/python_searcher.py b/hstest/testing/execution/searcher/python_searcher.py index f4f85c11..1afd32a0 100644 --- a/hstest/testing/execution/searcher/python_searcher.py +++ b/hstest/testing/execution/searcher/python_searcher.py @@ -1,32 +1,37 @@ +from __future__ import annotations + import os import re -from typing import Optional +from typing import TYPE_CHECKING from hstest.dynamic.output.output_handler import OutputHandler from hstest.testing.execution.filtering.file_filter import FileFilter, Folder, Sources from hstest.testing.execution.filtering.main_filter import MainFilter from hstest.testing.execution.runnable.python_runnable_file import PythonRunnableFile -from hstest.testing.execution.runnable.runnable_file import RunnableFile from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class PythonSearcher(BaseSearcher): +class PythonSearcher(BaseSearcher): @property def extension(self) -> str: - return '.py' + return ".py" - def search(self, where_to_search: str = None, file_filter: FileFilter = None) -> RunnableFile: + def search( + self, where_to_search: str | None = None, file_filter: FileFilter = None + ) -> RunnableFile: is_imported = {} - def init_regexes(_: Folder, sources: Sources): + def init_regexes(_: Folder, sources: Sources) -> None: import_regexes = {} for file, source in sources.items(): is_imported[file] = False import_regexes[file] = [ - re.compile(rf'(^|\n)import +[\w., ]*\b{file[:-3]}\b[\w., ]*', re.M), - re.compile(rf'(^|\n)from +\.? *\b{file[:-3]}\b +import +', re.M) + re.compile(rf"(^|\n)import +[\w., ]*\b{file[:-3]}\b[\w., ]*", re.MULTILINE), + re.compile(rf"(^|\n)from +\.? *\b{file[:-3]}\b +import +", re.MULTILINE), ] for file, source in sources.items(): @@ -37,22 +42,16 @@ def init_regexes(_: Folder, sources: Sources): return self._search( where_to_search, file_filter=file_filter, - - pre_main_filter=FileFilter( - init_files=init_regexes, - file=lambda f: not is_imported[f] - ), - + pre_main_filter=FileFilter(init_files=init_regexes, file=lambda f: not is_imported[f]), main_filter=MainFilter( - "if __name__ == '__main__'", - source=lambda s: '__name__' in s and '__main__' in s - ) + "if __name__ == '__main__'", source=lambda s: "__name__" in s and "__main__" in s + ), ) - def find(self, source: Optional[str]) -> PythonRunnableFile: - OutputHandler.print(f'PythonSearcher source = {source}, cwd = {os.getcwd()}') + def find(self, source: str | None) -> PythonRunnableFile: + OutputHandler.print(f"PythonSearcher source = {source}, cwd = {os.getcwd()}") runnable = super().find(source) - OutputHandler.print(f'PythonSearcher found runnable: {runnable.folder}/{runnable.file}') + OutputHandler.print(f"PythonSearcher found runnable: {runnable.folder}/{runnable.file}") return PythonRunnableFile( - runnable.folder, runnable.file, runnable.file[:-len(self.extension)] + runnable.folder, runnable.file, runnable.file[: -len(self.extension)] ) diff --git a/hstest/testing/execution/searcher/shell_searcher.py b/hstest/testing/execution/searcher/shell_searcher.py index effe194b..63f5a8a4 100644 --- a/hstest/testing/execution/searcher/shell_searcher.py +++ b/hstest/testing/execution/searcher/shell_searcher.py @@ -1,12 +1,17 @@ -from hstest.testing.execution.runnable.runnable_file import RunnableFile +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class ShellSearcher(BaseSearcher): +class ShellSearcher(BaseSearcher): @property def extension(self) -> str: - return '.sh' + return ".sh" - def search(self, where: str = None) -> RunnableFile: - return self._simple_search(where, "# main", r'(^|\n)# *main') + def search(self, where: str | None = None) -> RunnableFile: + return self._simple_search(where, "# main", r"(^|\n)# *main") diff --git a/hstest/testing/execution/searcher/sql_searcher.py b/hstest/testing/execution/searcher/sql_searcher.py index 986f2892..1f52c04f 100644 --- a/hstest/testing/execution/searcher/sql_searcher.py +++ b/hstest/testing/execution/searcher/sql_searcher.py @@ -1,12 +1,17 @@ -from hstest.testing.execution.runnable.runnable_file import RunnableFile +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class SQLSearcher(BaseSearcher): +class SQLSearcher(BaseSearcher): @property def extension(self) -> str: - return '.sql' + return ".sql" - def search(self, where: str = None) -> RunnableFile: + def search(self, where: str | None = None) -> RunnableFile: return self._base_search(where) diff --git a/hstest/testing/execution_options.py b/hstest/testing/execution_options.py index da688918..d82273d6 100644 --- a/hstest/testing/execution_options.py +++ b/hstest/testing/execution_options.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import os import sys -skip_slow: bool = '--skip_slow' in sys.argv -ignore_stdout: bool = '--ignore_stdout' in sys.argv -inside_docker: bool = '--inside_docker' in sys.argv or os.environ.get('INSIDE_DOCKER', '') == '1' -debug_mode: bool = '--debug_mode' in sys.argv or sys.gettrace() is not None -force_process_testing: bool = '--force_process_testing' in sys.argv +skip_slow: bool = "--skip_slow" in sys.argv +ignore_stdout: bool = "--ignore_stdout" in sys.argv +inside_docker: bool = "--inside_docker" in sys.argv or os.environ.get("INSIDE_DOCKER", "") == "1" +debug_mode: bool = "--debug_mode" in sys.argv or sys.gettrace() is not None +force_process_testing: bool = "--force_process_testing" in sys.argv diff --git a/hstest/testing/plotting/__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..6296a0bd 100644 --- a/hstest/testing/plotting/drawing/drawing_data.py +++ b/hstest/testing/plotting/drawing/drawing_data.py @@ -1,25 +1,27 @@ -from typing import Optional +from __future__ import annotations import numpy as np class DrawingData: - def __init__(self, x: np.ndarray, y: np.ndarray): + def __init__(self, x: np.ndarray, y: np.ndarray) -> None: try: if type(x) != list and x is not None: x = list(x) if type(y) != list and y is not None: y = list(y) except Exception: - raise ValueError('The data argument should be an array') + msg = "The data argument should be an array" + raise ValueError(msg) if x is not None and y is not None and len(x) != len(y): - raise ValueError('Arrays should be the same length') + msg = "Arrays should be the same length" + raise ValueError(msg) if x is not None: x = np.array(x, dtype=object) if y is not None: y = np.array(y, dtype=object) - self.x: Optional[np.ndarray] = x - self.y: Optional[np.ndarray] = y + self.x: np.ndarray | None = x + self.y: np.ndarray | None = y diff --git a/hstest/testing/plotting/drawing/drawing_library.py b/hstest/testing/plotting/drawing/drawing_library.py index 6f1d5730..6af09a3a 100644 --- a/hstest/testing/plotting/drawing/drawing_library.py +++ b/hstest/testing/plotting/drawing/drawing_library.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class DrawingLibrary: matplotlib = "matplotlib" pandas = "pandas" diff --git a/hstest/testing/plotting/drawing/drawing_type.py b/hstest/testing/plotting/drawing/drawing_type.py index 0c95a5d7..f3febb89 100644 --- a/hstest/testing/plotting/drawing/drawing_type.py +++ b/hstest/testing/plotting/drawing/drawing_type.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class DrawingType: # ---------------------- # common types with data diff --git a/hstest/testing/plotting/drawing_data_normalizer.py b/hstest/testing/plotting/drawing_data_normalizer.py index e30928df..a0e0639c 100644 --- a/hstest/testing/plotting/drawing_data_normalizer.py +++ b/hstest/testing/plotting/drawing_data_normalizer.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import numpy as np class DrawingDataNormalizer: - @staticmethod def normalize_x_y_data(x, y) -> np.ndarray: try: @@ -11,21 +12,22 @@ def normalize_x_y_data(x, y) -> np.ndarray: if type(y) != list: y = list(y) except Exception: - raise ValueError('The data argument should be an array') + msg = "The data argument should be an array" + raise ValueError(msg) if len(x) != len(y): - raise ValueError('Arrays should be the same length') + msg = "Arrays should be the same length" + raise ValueError(msg) - result_data = list() + result_data = [] - for a, b in zip(x, y): + for a, b in zip(x, y, strict=False): result_data.append((a, b)) return np.array(result_data, dtype=object) @staticmethod def normalize_hist_data(data) -> np.ndarray: - if type(data) == str: data = [data] @@ -33,7 +35,8 @@ def normalize_hist_data(data) -> np.ndarray: try: data = list(data) except Exception: - raise ValueError('The data argument should be an array') + msg = "The data argument should be an array" + raise ValueError(msg) return np.array(data, dtype=object) @@ -73,6 +76,7 @@ def normalize_hist_data(data) -> np.ndarray: return np.array(result_data, dtype=object) """ # noqa: W293 + return None @staticmethod def normalize_bar_data(x, y) -> np.ndarray: diff --git a/hstest/testing/plotting/matplotlib_handler.py b/hstest/testing/plotting/matplotlib_handler.py index 32388b3e..d3c1f269 100644 --- a/hstest/testing/plotting/matplotlib_handler.py +++ b/hstest/testing/plotting/matplotlib_handler.py @@ -1,13 +1,16 @@ +from __future__ import annotations + +import contextlib from copy import deepcopy from importlib import reload from typing import TYPE_CHECKING from hstest.testing.plotting.drawing.drawing_data import DrawingData -try: +with contextlib.suppress(ImportError): import pandas as pd -except ImportError: - pass + +import contextlib from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.drawing.drawing_builder import DrawingBuilder @@ -37,30 +40,27 @@ class MatplotlibHandler: _matplotlib = None @staticmethod - def replace_plots(drawings: 'DrawingsStorage'): - + def replace_plots(drawings: DrawingsStorage) -> None: try: - import matplotlib + import matplotlib as mpl import numpy as np except ModuleNotFoundError: return - def custom_show_func(*args, **kwargs): + def custom_show_func(*args, **kwargs) -> None: pass def hist(x, *args, data=None, **kw): if data is not None: - try: + with contextlib.suppress(Exception): x = data[x] - except Exception: - pass try: if type(x) == pd.DataFrame: for col in x.columns: hist(x[col], *args, **kw) - return - elif type(x) == pd.Series: + return None + if type(x) == pd.Series: return hist(x.to_numpy(), *args, **kw) except Exception: pass @@ -68,128 +68,111 @@ def hist(x, *args, data=None, **kw): if type(x) != np.ndarray: x = np.array(x, dtype=object) if len(x.shape) == 2: - import matplotlib.cbook as cbook - x = np.array(cbook._reshape_2D(x, 'x'), dtype=object) + from matplotlib import cbook + + x = np.array(cbook._reshape_2D(x, "x"), dtype=object) if len(x.shape) == 2: for i in range(x.shape[1]): hist(x[:, i], *args, **kw) - return + return None drawings.append( Drawing( DrawingLibrary.matplotlib, DrawingType.hist, DrawingData(x, np.array([1] * len(x), dtype=object)), - kw + kw, ) ) + return None def bar(x, height, *args, data=None, **kw): if data is not None: - try: + with contextlib.suppress(Exception): x = data[x] - except Exception: - pass - try: + with contextlib.suppress(Exception): height = data[height] - except Exception: - pass try: if type(x) == pd.DataFrame: for col in x.columns: bar(x[col], *args, **kw) - return - elif type(x) == pd.Series: + return None + if type(x) == pd.Series: return bar(x.to_numpy(), height, *args, **kw) - elif type(height) == pd.Series: + if type(height) == pd.Series: return bar(x, height.to_numpy(), *args, **kw) except Exception: pass - if type(height) in [int, float]: + if type(height) in {int, float}: height = np.full((len(x),), height) drawings.append( - Drawing( - DrawingLibrary.matplotlib, - DrawingType.bar, - DrawingData(x, height), - kw - ) + Drawing(DrawingLibrary.matplotlib, DrawingType.bar, DrawingData(x, height), kw) ) + return None def barh(x, width, *args, data=None, **kw): return bar(x, width, *args, data=data, **kw) - def plot(*args, **kwargs): - x = list() - y = list() + def plot(*args, **kwargs) -> None: + x = [] + y = [] - if len(args) > 0: - if type(args[0]) is list: - x = args[0] + if len(args) > 0 and type(args[0]) is list: + x = args[0] if len(args) > 1: if type(args[1]) is list: y = args[1] else: - y = [_ for _ in range(len(x))] + y = list(range(len(x))) drawings.append( DrawingBuilder.get_line_drawing( - x, y, + x, + y, DrawingLibrary.matplotlib, kwargs, ) ) - def scatter(x, y, *a, **kwargs): + def scatter(x, y, *a, **kwargs) -> None: drawings.append( DrawingBuilder.get_scatter_drawing( - x, y, + x, + y, DrawingLibrary.matplotlib, kwargs, ) ) - def pie(x, *a, **kw): + def pie(x, *a, **kw) -> None: # Normalize with other plot libraries y = x - x = [''] * len(y) + x = [""] * len(y) - if 'labels' in kw and kw['labels'] is not None: - x = kw['labels'] + if "labels" in kw and kw["labels"] is not None: + x = kw["labels"] drawings.append( - Drawing( - DrawingLibrary.matplotlib, - DrawingType.pie, - DrawingData(x, y), - kw - ) + Drawing(DrawingLibrary.matplotlib, DrawingType.pie, DrawingData(x, y), kw) ) - def violinplot(dataset, *, data=None, **kwargs): + def violinplot(dataset, *, data=None, **kwargs) -> None: if data is not None: - try: + with contextlib.suppress(Exception): dataset = data[dataset] - except Exception: - pass - drawing = Drawing( - DrawingLibrary.matplotlib, - DrawingType.violin, - dataset, - kwargs - ) + drawing = Drawing(DrawingLibrary.matplotlib, DrawingType.violin, dataset, kwargs) drawings.append(drawing) - def imshow(x, **kwargs): + def imshow(x, **kwargs) -> None: curr_data = { # noqa: F841 - 'x': np.array(x, dtype=object) + "x": np.array(x, dtype=object) } drawing = Drawing( @@ -200,10 +183,10 @@ def imshow(x, **kwargs): ) drawings.append(drawing) - def boxplot(x, **kwargs): + def boxplot(x, **kwargs) -> None: curr_data = { # noqa: F841 - 'x': np.array([None], dtype=object), - 'y': np.array(x, dtype=object) + "x": np.array([None], dtype=object), + "y": np.array(x, dtype=object), } drawing = Drawing( @@ -214,39 +197,37 @@ def boxplot(x, **kwargs): ) drawings.append(drawing) + import matplotlib as mpl import matplotlib.axes class CustomMatplotlibAxes(matplotlib.axes.Axes): - - def hist(self, x, *a, **kw): + def hist(self, x, *a, **kw) -> None: hist(x, *a, **kw) - def bar(self, x, height, *a, **kw): + def bar(self, x, height, *a, **kw) -> None: bar(x, height, *a, **kw) - def barh(self, y, width, *a, **kw): + def barh(self, y, width, *a, **kw) -> None: barh(y, width, *a, **kw) - def plot(self, *args, **kwargs): + def plot(self, *args, **kwargs) -> None: plot(*args, *kwargs) - def scatter(self, x, y, *a, **kwargs): + def scatter(self, x, y, *a, **kwargs) -> None: scatter(x, y, *a, **kwargs) - def pie(self, x, *a, **kw): + def pie(self, x, *a, **kw) -> None: pie(x, *a, **kw) - def violinplot(self, dataset, **kwargs): + def violinplot(self, dataset, **kwargs) -> None: violinplot(dataset, **kwargs) - def imshow(self, x, **kwargs): + def imshow(self, x, **kwargs) -> None: imshow(x, **kwargs) - def boxplot(self, x, **kwargs): + def boxplot(self, x, **kwargs) -> None: boxplot(x, **kwargs) - import matplotlib - if not MatplotlibHandler._saved: MatplotlibHandler._Axes = deepcopy(matplotlib.axes.Axes) @@ -254,7 +235,8 @@ def boxplot(self, x, **kwargs): matplotlib.axes.Axes = CustomMatplotlibAxes from matplotlib.projections import projection_registry - projection_registry.register(matplotlib.axes.Axes) + + projection_registry.register(CustomMatplotlibAxes) import matplotlib.pyplot as plt @@ -270,7 +252,7 @@ def boxplot(self, x, **kwargs): MatplotlibHandler._imshow = plt.imshow MatplotlibHandler._boxplot = plt.boxplot MatplotlibHandler._show = plt.show - MatplotlibHandler._backend = matplotlib.get_backend() + MatplotlibHandler._backend = mpl.get_backend() plt.hist = hist plt.plot = plot @@ -283,13 +265,12 @@ def boxplot(self, x, **kwargs): plt.boxplot = boxplot plt.show = custom_show_func - matplotlib.use('Agg') + mpl.use("Agg") MatplotlibHandler._replaced = True @staticmethod - def revert_plots(): - + def revert_plots() -> None: if not MatplotlibHandler._replaced: return diff --git a/hstest/testing/plotting/pandas_handler.py b/hstest/testing/plotting/pandas_handler.py index 98e9100a..ad1a6168 100644 --- a/hstest/testing/plotting/pandas_handler.py +++ b/hstest/testing/plotting/pandas_handler.py @@ -1,11 +1,12 @@ +from __future__ import annotations + +import contextlib from typing import TYPE_CHECKING from hstest.testing.plotting.drawing.drawing_data import DrawingData -try: +with contextlib.suppress(ImportError): import numpy as np -except ImportError: - pass try: import pandas as pd @@ -13,6 +14,8 @@ except ImportError: pass +import contextlib + from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.drawing.drawing_builder import DrawingBuilder from hstest.testing.plotting.drawing.drawing_library import DrawingLibrary @@ -43,67 +46,40 @@ class PandasHandler: plot_name_to_basic_name = { # 'barh': DrawingType.bar, - 'density': DrawingType.dis, - 'kde': DrawingType.dis, - } - - graph_type_to_normalized_data = { - 'scatter': lambda data, x, y: PandasHandler.get_scatter_drawings_with_normalized_data( - data, x, y - ), - 'line': lambda data, x, y: PandasHandler.get_line_drawings_with_normalized_data(data, x, y), - 'pie': lambda data, x, y: PandasHandler.get_pie_drawings_with_normalized_data(data, x, y), - # 'bar': lambda data, x, y: PandasHandler.get_bar_drawings_with_normalized_data(data, x, y), - 'box': lambda data, x, y: PandasHandler.get_box_drawings_with_normalized_data(data, x, y), - 'dis': lambda data, x, y: PandasHandler.get_dis_drawings_with_normalized_data(data, x, y), + "density": DrawingType.dis, + "kde": DrawingType.dis, } @staticmethod def get_line_drawings_with_normalized_data(data, x, y): - drawings = list() + drawings = [] if type(data) is pd.Series: drawings.append( - DrawingBuilder.get_line_drawing( - data.index, - data, - DrawingLibrary.pandas, - {} - ) + DrawingBuilder.get_line_drawing(data.index, data, DrawingLibrary.pandas, {}) ) return drawings for column in data.columns: drawings.append( - DrawingBuilder.get_line_drawing( - data.index, - data[column], - DrawingLibrary.pandas, - {} - ) + DrawingBuilder.get_line_drawing(data.index, data[column], DrawingLibrary.pandas, {}) ) return drawings @staticmethod def get_scatter_drawings_with_normalized_data(data, x, y): - return [ - DrawingBuilder.get_scatter_drawing( - data[x], data[y], - DrawingLibrary.pandas, - {} - ) - ] + return [DrawingBuilder.get_scatter_drawing(data[x], data[y], DrawingLibrary.pandas, {})] @staticmethod - def get_pie_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): + def get_pie_drawings_with_normalized_data(data: pd.DataFrame, x, y): if type(data) == pd.Series: return [ Drawing( DrawingLibrary.pandas, DrawingType.pie, DrawingData(data.index.to_numpy(), data.to_numpy()), - {} + {}, ) ] @@ -113,7 +89,7 @@ def get_pie_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): DrawingLibrary.pandas, DrawingType.pie, DrawingData(data.index.to_numpy(), data[y].to_numpy()), - {} + {}, ) ] @@ -127,43 +103,31 @@ def get_pie_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): DrawingLibrary.pandas, DrawingType.pie, DrawingData(data.index.to_numpy(), data[column].to_numpy()), - {} + {}, ) ) return drawings @staticmethod - def get_bar_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): + def get_bar_drawings_with_normalized_data(data: pd.DataFrame, x, y): drawings = [] - if x is not None: - x_arr = data[x].to_numpy() - else: - x_arr = data.index.to_numpy() + x_arr = data[x].to_numpy() if x is not None else data.index.to_numpy() if y is not None: - drawing = DrawingBuilder.get_bar_drawing( - x_arr, data[y], - DrawingLibrary.pandas, - {} - ) + drawing = DrawingBuilder.get_bar_drawing(x_arr, data[y], DrawingLibrary.pandas, {}) drawings.append(drawing) return drawings for column in data.columns: if not is_numeric_dtype(data[column]): continue - drawing = DrawingBuilder.get_bar_drawing( - x_arr, data[column], - DrawingLibrary.pandas, - {} - ) + drawing = DrawingBuilder.get_bar_drawing(x_arr, data[column], DrawingLibrary.pandas, {}) drawings.append(drawing) return drawings @staticmethod - def get_box_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): - + def get_box_drawings_with_normalized_data(data: pd.DataFrame, x, y): drawings = [] # Columns are not specified @@ -172,17 +136,9 @@ def get_box_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): if not is_numeric_dtype(data[column]): continue - curr_data = { # noqa: F841 - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() - } + curr_data = {"x": np.array([column], dtype=object), "y": data[column].to_numpy()} - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.box, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.box, None, {}) drawings.append(drawing) return drawings @@ -191,16 +147,11 @@ def get_box_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): continue curr_data = { # noqa: F841 - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() + "x": np.array([column], dtype=object), + "y": data[column].to_numpy(), } - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.box, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.box, None, {}) drawings.append(drawing) return drawings @@ -209,42 +160,25 @@ def get_dis_drawings_with_normalized_data(data, x, y): drawings = [] if type(data) == pd.Series: - curr_data = { - 'x': data.to_numpy() - } + curr_data = {"x": data.to_numpy()} - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.dis, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) return drawings if x: curr_data = { - 'x': np.array(data[x], dtype=object), + "x": np.array(data[x], dtype=object), } - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.dis, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) if y: curr_data = { - 'x': np.array(data[y], dtype=object), + "x": np.array(data[y], dtype=object), } - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.dis, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) if not x and not y: @@ -253,44 +187,29 @@ def get_dis_drawings_with_normalized_data(data, x, y): continue curr_data = { # noqa: F841 - 'x': data[column].to_numpy() + "x": data[column].to_numpy() } - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.dis, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) return drawings @staticmethod def get_area_drawings_with_normalized_data(data, x, y): drawings = [] - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.area, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.area, None, {}) drawings.append(drawing) return drawings @staticmethod def get_hexbin_drawings_with_normalized_data(data, x, y): drawings = [] - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.hexbin, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.hexbin, None, {}) drawings.append(drawing) return drawings @staticmethod - def replace_plots(drawings: 'DrawingsStorage'): + def replace_plots(drawings: DrawingsStorage) -> None: try: import pandas.plotting from pandas.core.accessor import CachedAccessor @@ -308,22 +227,21 @@ def __call__(self, *args, **kw): ) if kind not in self._all_kinds: - raise ValueError(f"{kind} is not a valid plot kind") + msg = f"{kind} is not a valid plot kind" + raise ValueError(msg) data = self._parent.copy() - plot_name = kind if kind not in PandasHandler.plot_name_to_basic_name \ - else PandasHandler.plot_name_to_basic_name[kind] + plot_name = PandasHandler.plot_name_to_basic_name.get(kind, kind) # For boxplot from plot accessor - if plot_name == DrawingType.box: - if 'columns' in kwargs: - x = kwargs['columns'] + if plot_name == DrawingType.box and "columns" in kwargs: + x = kwargs["columns"] plot_to_func = { - 'hist': hist, - 'bar': bar, - 'barh': barh, + "hist": hist, + "bar": bar, + "barh": barh, } if plot_name in PandasHandler.graph_type_to_normalized_data: @@ -335,102 +253,79 @@ def __call__(self, *args, **kw): plot_to_func[plot_name](data, **kw) else: curr_data = { # noqa: F841 - 'data': data, - 'x': x, - 'y': y, - 'kwargs': kwargs + "data": data, + "x": x, + "y": y, + "kwargs": kwargs, } - drawing = Drawing( - DrawingLibrary.pandas, - plot_name, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, plot_name, None, {}) drawings.append(drawing) import pandas.plotting._core - def boxplot( - self, - column=None, - **kwargs - ): + def boxplot(self, column=None, **kwargs) -> None: all_drawings = PandasHandler.get_box_drawings_with_normalized_data(self, column, None) drawings.extend(all_drawings) - def hist( - data, - column=None, - _process_by=True, - **kw - ): + def hist(data, column=None, _process_by=True, **kw): for k in list(kw.keys()): if kw[k] is None: kw.pop(k) - if _process_by and 'by' in kw and type(kw['by']) == str: - try: - kw['by'] = data[kw['by']] - except Exception: - pass + if _process_by and "by" in kw and type(kw["by"]) == str: + with contextlib.suppress(Exception): + kw["by"] = data[kw["by"]] - if 'y' in kw: - try: - data = data[kw.pop('y')] - except Exception: - pass + if "y" in kw: + with contextlib.suppress(Exception): + data = data[kw.pop("y")] - if 'x' in kw: - try: - data = data[kw.pop('x')] - except Exception: - pass + if "x" in kw: + with contextlib.suppress(Exception): + data = data[kw.pop("x")] if type(data) == pandas.DataFrame: if column is not None: return hist(data[column].to_numpy(), **kw) for col in data.columns: hist(data[col].to_numpy(), **kw) - return + return None - elif type(data) == pandas.Series: + if type(data) == pandas.Series: return hist(data.to_numpy(), **kw) - elif type(data) != np.ndarray: + if type(data) != np.ndarray: data = np.array(data, dtype=object) if len(data.shape) == 2: - import matplotlib.cbook as cbook - data = np.array(cbook._reshape_2D(data, 'x'), dtype=object) + from matplotlib import cbook + + data = np.array(cbook._reshape_2D(data, "x"), dtype=object) if len(data.shape) == 2: for i in range(data.shape[1]): hist(data[:, i], **kw) - return + return None - if _process_by and 'by' in kw: - by = kw['by'] + if _process_by and "by" in kw: + by = kw["by"] pictures = sorted(set(by), key=str) for pic in pictures: - subplot = [i for i, j in zip(data, by) if j == pic] + subplot = [i for i, j in zip(data, by, strict=False) if j == pic] hist(np.array(subplot, dtype=object), _process_by=False, **kw) - return + return None drawings.append( Drawing( DrawingLibrary.pandas, DrawingType.hist, DrawingData(data, np.array([1] * len(data), dtype=object)), - kw + kw, ) ) + return None - def bar( - data, - x=None, - y=None, - **kw - ): + def bar(data, x=None, y=None, **kw): for k in list(kw.keys()): if kw[k] is None: kw.pop(k) @@ -440,57 +335,35 @@ def bar( if type(y) == str: y = [y] for col in y: - bar(None, - data[x].array.to_numpy(), - data[col].array.to_numpy(), - **kw) - return + bar(None, data[x].array.to_numpy(), data[col].array.to_numpy(), **kw) + return None - elif x is not None: + if x is not None: for col in data.columns: if col != x: - bar(None, - data[x].array.to_numpy(), - data[col].array.to_numpy(), - **kw) - return + bar(None, data[x].array.to_numpy(), data[col].array.to_numpy(), **kw) + return None - elif y is not None: + if y is not None: if type(y) == str: y = [y] for col in y: - bar(None, - data[col].index.to_numpy(), - data[col].array.to_numpy(), - **kw) - return + bar(None, data[col].index.to_numpy(), data[col].array.to_numpy(), **kw) + return None - else: - for col in data.columns: - bar(None, - data[col].index.to_numpy(), - data[col].array.to_numpy(), - **kw) - return - - elif type(data) == pandas.Series: - return bar(None, - data.index.to_numpy(), - data.array.to_numpy(), - **kw) + for col in data.columns: + bar(None, data[col].index.to_numpy(), data[col].array.to_numpy(), **kw) + return None - drawings.append( - Drawing( - DrawingLibrary.pandas, - DrawingType.bar, - DrawingData(x, y), - kw - ) - ) + if type(data) == pandas.Series: + return bar(None, data.index.to_numpy(), data.array.to_numpy(), **kw) + + drawings.append(Drawing(DrawingLibrary.pandas, DrawingType.bar, DrawingData(x, y), kw)) + return None def barh( self, - ): + ) -> None: pass if not PandasHandler._saved: @@ -517,7 +390,7 @@ def barh( PandasHandler._replaced = True @staticmethod - def revert_plots(): + def revert_plots() -> None: if not PandasHandler._replaced: return @@ -535,3 +408,12 @@ def revert_plots(): pandas.DataFrame.boxplot = PandasHandler._dframe_boxplot PandasHandler._replaced = False + + graph_type_to_normalized_data = { + "scatter": get_scatter_drawings_with_normalized_data, + "line": get_line_drawings_with_normalized_data, + "pie": get_pie_drawings_with_normalized_data, + # 'bar': lambda data, x, y: get_bar_drawings_with_normalized_data(data, x, y), + "box": get_box_drawings_with_normalized_data, + "dis": get_dis_drawings_with_normalized_data, + } diff --git a/hstest/testing/plotting/seaborn_handler.py b/hstest/testing/plotting/seaborn_handler.py index b5b773b3..ad0c5951 100644 --- a/hstest/testing/plotting/seaborn_handler.py +++ b/hstest/testing/plotting/seaborn_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from importlib import reload from typing import TYPE_CHECKING @@ -9,6 +11,8 @@ except ImportError: pass +import contextlib + from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.drawing.drawing_builder import DrawingBuilder from hstest.testing.plotting.drawing.drawing_library import DrawingLibrary @@ -35,22 +39,19 @@ class SeabornHandler: _boxplot = None @staticmethod - def replace_plots(drawings: 'DrawingsStorage'): + def replace_plots(drawings: DrawingsStorage) -> None: try: import numpy as np import seaborn as sns except ModuleNotFoundError: return - def displot(data=None, **kwargs): - x = None if 'x' not in kwargs else kwargs['x'] - y = None if 'y' not in kwargs else kwargs['y'] + def displot(data=None, **kwargs) -> None: + x = kwargs.get("x") + y = kwargs.get("y") if data is None: - curr_data = { - 'x': np.array(x, dtype=object), - 'y': np.array(y, dtype=object) - } + curr_data = {"x": np.array(x, dtype=object), "y": np.array(y, dtype=object)} drawing = Drawing( DrawingLibrary.seaborn, @@ -70,8 +71,8 @@ def displot(data=None, **kwargs): continue curr_data = { - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() + "x": np.array([column], dtype=object), + "y": data[column].to_numpy(), } drawing = Drawing( @@ -89,8 +90,8 @@ def displot(data=None, **kwargs): y_arr = data[y].to_numpy() curr_data = { # noqa: F841 - 'x': np.array(x_arr, dtype=object), - 'y': np.array(y_arr, dtype=object) + "x": np.array(x_arr, dtype=object), + "y": np.array(y_arr, dtype=object), } drawing = Drawing( @@ -103,66 +104,59 @@ def displot(data=None, **kwargs): def histplot(data=None, _process_hue=True, **kw): if data is None: - return + return None - if _process_hue and 'hue' in kw and type(kw['hue']) == str: - try: - kw['hue'] = data[kw['hue']] - except Exception: - pass + if _process_hue and "hue" in kw and type(kw["hue"]) == str: + with contextlib.suppress(Exception): + kw["hue"] = data[kw["hue"]] - if 'y' in kw: - try: - data = data[kw.pop('y')] - except Exception: - pass + if "y" in kw: + with contextlib.suppress(Exception): + data = data[kw.pop("y")] - if 'x' in kw: - try: - data = data[kw.pop('x')] - except Exception: - pass + if "x" in kw: + with contextlib.suppress(Exception): + data = data[kw.pop("x")] if type(data) == pd.DataFrame: for col in data.columns: histplot(data[col], **kw) - return - elif type(data) == pd.Series: + return None + if type(data) == pd.Series: return histplot(data.to_numpy(), **kw) - elif type(data) != np.ndarray: + if type(data) != np.ndarray: data = np.array(data, dtype=object) if len(data.shape) == 2: - import matplotlib.cbook as cbook - data = np.array(cbook._reshape_2D(data, 'x'), dtype=object) + from matplotlib import cbook + + data = np.array(cbook._reshape_2D(data, "x"), dtype=object) if len(data.shape) == 2: for i in range(data.shape[1]): histplot(data[:, i], **kw) - return + return None - if _process_hue and 'hue' in kw: - hue = kw['hue'] + if _process_hue and "hue" in kw: + hue = kw["hue"] colored_layers = sorted(set(hue), key=str) for pic in colored_layers: - subplot = [i for i, j in zip(data, hue) if j == pic] + subplot = [i for i, j in zip(data, hue, strict=False) if j == pic] histplot(np.array(subplot, dtype=object), _process_hue=False, **kw) - return + return None drawings.append( Drawing( DrawingLibrary.seaborn, DrawingType.hist, DrawingData(data, np.array([1] * len(data), dtype=object)), - kw + kw, ) ) + return None def lineplot(*, data=None, x=None, y=None, **kwargs): - if x is not None: - x_array = data[x].to_numpy() - else: - x_array = data.index.to_numpy() + x_array = data[x].to_numpy() if x is not None else data.index.to_numpy() if y is not None: y_array = data[y].to_numpy() @@ -189,13 +183,14 @@ def lineplot(*, data=None, x=None, y=None, **kwargs): kwargs, ) ) + return None - def lmplot(x=None, y=None, data=None, **kwargs): + def lmplot(x=None, y=None, data=None, **kwargs) -> None: curr_data = { # noqa: F841 - 'data': data, - 'x': x, - 'y': y, - 'kwargs': kwargs + "data": data, + "x": x, + "y": y, + "kwargs": kwargs, } drawing = Drawing( @@ -206,11 +201,12 @@ def lmplot(x=None, y=None, data=None, **kwargs): ) drawings.append(drawing) - def scatterplot(x=None, y=None, data=None, **kwargs): + def scatterplot(x=None, y=None, data=None, **kwargs) -> None: if x is not None and y is not None: drawings.append( DrawingBuilder.get_scatter_drawing( - data[x], data[y], + data[x], + data[y], DrawingLibrary.seaborn, kwargs, ) @@ -225,18 +221,19 @@ def scatterplot(x=None, y=None, data=None, **kwargs): x = data.index drawings.append( DrawingBuilder.get_scatter_drawing( - x, data[column], + x, + data[column], DrawingLibrary.seaborn, kwargs, ) ) - def catplot(x=None, y=None, data=None, **kwargs): + def catplot(x=None, y=None, data=None, **kwargs) -> None: curr_data = { # noqa: F841 - 'data': data, - 'x': x, - 'y': y, - 'kwargs': kwargs + "data": data, + "x": x, + "y": y, + "kwargs": kwargs, } drawing = Drawing( @@ -247,30 +244,23 @@ def catplot(x=None, y=None, data=None, **kwargs): ) drawings.append(drawing) - def barplot(x=None, y=None, data=None, **kwargs): - + def barplot(x=None, y=None, data=None, **kwargs) -> None: x_arr = np.array([], dtype=object) y_arr = np.array([], dtype=object) if data is not None: if x: x_arr = data[x].to_numpy() - y_arr = np.full((x_arr.size,), '', dtype=str) + y_arr = np.full((x_arr.size,), "", dtype=str) if y: y_arr = data[y].to_numpy() if x_arr.size == 0: - x_arr = np.full((y_arr.size,), '', dtype=str) + x_arr = np.full((y_arr.size,), "", dtype=str) drawings.append( - Drawing( - DrawingLibrary.seaborn, - DrawingType.bar, - DrawingData(x_arr, y_arr), - kwargs - ) + Drawing(DrawingLibrary.seaborn, DrawingType.bar, DrawingData(x_arr, y_arr), kwargs) ) - def violinplot(*, x=None, y=None, data=None, **kwargs): - + def violinplot(*, x=None, y=None, data=None, **kwargs) -> None: if data is not None: if x is None and y is not None: data = data[y] @@ -278,13 +268,12 @@ def violinplot(*, x=None, y=None, data=None, **kwargs): data = data[x] elif x is not None and y is not None: data = pd.concat([data[x], data[y]], axis=1).reset_index() + elif x is None: + data = y + elif y is None: + data = x else: - if x is None: - data = y - elif y is None: - data = x - else: - data = pd.concat([x, y], axis=1).reset_index() + data = pd.concat([x, y], axis=1).reset_index() drawing = Drawing( DrawingLibrary.seaborn, @@ -295,12 +284,12 @@ def violinplot(*, x=None, y=None, data=None, **kwargs): drawings.append(drawing) - def heatmap(data=None, **kwargs): + def heatmap(data=None, **kwargs) -> None: if data is None: return curr_data = { # noqa: F841 - 'x': np.array(data, dtype=object) + "x": np.array(data, dtype=object) } drawing = Drawing( @@ -312,13 +301,9 @@ def heatmap(data=None, **kwargs): drawings.append(drawing) - def boxplot(x=None, y=None, data=None, **kwargs): - + def boxplot(x=None, y=None, data=None, **kwargs) -> None: if data is None: - curr_data = { - 'x': np.array(x, dtype=object), - 'y': np.array(y, dtype=object) - } + curr_data = {"x": np.array(x, dtype=object), "y": np.array(y, dtype=object)} drawing = Drawing( DrawingLibrary.seaborn, @@ -339,8 +324,8 @@ def boxplot(x=None, y=None, data=None, **kwargs): continue curr_data = { - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() + "x": np.array([column], dtype=object), + "y": data[column].to_numpy(), } drawing = Drawing( @@ -358,8 +343,8 @@ def boxplot(x=None, y=None, data=None, **kwargs): y_arr = data[y].to_numpy() curr_data = { # noqa: F841 - 'x': np.array(x_arr, dtype=object), - 'y': np.array(y_arr, dtype=object) + "x": np.array(x_arr, dtype=object), + "y": np.array(y_arr, dtype=object), } drawing = Drawing( @@ -397,8 +382,7 @@ def boxplot(x=None, y=None, data=None, **kwargs): SeabornHandler._replaced = True @staticmethod - def revert_plots(): - + def revert_plots() -> None: if not SeabornHandler._replaced: return diff --git a/hstest/testing/process_wrapper.py b/hstest/testing/process_wrapper.py index 3b5866c1..c4988181 100644 --- a/hstest/testing/process_wrapper.py +++ b/hstest/testing/process_wrapper.py @@ -1,8 +1,10 @@ +from __future__ import annotations + +import os import subprocess import sys from threading import Lock, Thread from time import sleep -from typing import Optional from psutil import NoSuchProcess, Process @@ -19,17 +21,18 @@ class ProcessWrapper: initial_idle_wait = True initial_idle_wait_time = 150 - def __init__(self, *args, check_early_finish=False, register_output=True, - register_io_handler=False): + def __init__( + self, *args, check_early_finish=False, register_output=True, register_io_handler=False + ) -> None: self.lock = Lock() self.args = args - self.process: Optional[subprocess.Popen] = None - self.ps: Optional[Process] = None + self.process: subprocess.Popen | None = None + self.ps: Process | None = None - self.stdout = '' - self.stderr = '' + self.stdout = "" + self.stderr = "" self._alive = True self._pipes_watching = 0 self.terminated = False @@ -48,22 +51,32 @@ def __init__(self, *args, check_early_finish=False, register_output=True, self._group = None def start(self): - command = ' '.join(map(str, self.args)) + command = " ".join(map(str, self.args)) if self.process is not None: - raise UnexpectedError(f"Cannot start the same process twice\n\"{command}\"") + msg = f'Cannot start the same process twice\n"{command}"' + raise UnexpectedError(msg) try: args = [str(a) for a in self.args] - if is_windows(): - if args[0] == 'bash': - # bash doesn't like Windows' \r\n, - # so we use byte stream instead of text stream - # to communicate between processes - self._use_byte_stream = True + if is_windows() and args[0] == "bash": + # bash doesn't like Windows' \r\n, + # so we use byte stream instead of text stream + # to communicate between processes + self._use_byte_stream = True + + args = ["cmd", "/c", *args] - args = ['cmd', '/c'] + args + # Set environment variables for proper encoding on Windows + env = os.environ.copy() + if is_windows(): + env.update( + { + "PYTHONIOENCODING": "utf-8", + "PYTHONLEGACYWINDOWSSTDIO": "0", # Disable legacy stdio behavior on Windows + } + ) self.process = subprocess.Popen( args, @@ -72,12 +85,16 @@ def start(self): stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, - encoding='utf-8' if not self._use_byte_stream else None, + encoding="utf-8" if not self._use_byte_stream else None, + errors="replace", # Handle encoding errors gracefully + env=env, ) except Exception as e: from hstest import StageTest + StageTest.curr_test_run.set_error_in_test( - UnexpectedError(f"Cannot start process\n\"{command}\"", e)) + UnexpectedError(f'Cannot start process\n"{command}"', e) + ) self._alive = False self.terminated = True return self @@ -87,30 +104,29 @@ def start(self): if self.register_io_handler: self._group = ThreadGroup() SystemHandler.install_handler( - self, - lambda: ThreadGroup.curr_group() == self._group, - lambda: None + self, lambda: ThreadGroup.curr_group() == self._group, lambda: None ) - Thread(target=lambda: self.check_cpuload(), daemon=True, group=self._group).start() - Thread(target=lambda: self.check_output(), daemon=True, group=self._group).start() - Thread(target=lambda: self.check_stdout(), daemon=True, group=self._group).start() - Thread(target=lambda: self.check_stderr(), daemon=True, group=self._group).start() + Thread(target=self.check_cpuload, daemon=True, group=self._group).start() + Thread(target=self.check_output, daemon=True, group=self._group).start() + Thread(target=self.check_stdout, daemon=True, group=self._group).start() + Thread(target=self.check_stderr, daemon=True, group=self._group).start() return self - def check_alive(self): + def check_alive(self) -> None: if self._alive and self.process.returncode is not None: self._alive = False - def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=False): + def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=False) -> None: pipe_name = "stdout" if write_stdout else "stderr" with self.lock: self._pipes_watching += 1 - OutputHandler.print(f'Start watching {pipe_name} ' - f'Pipes watching = {self._pipes_watching}') + OutputHandler.print( + f"Start watching {pipe_name} " f"Pipes watching = {self._pipes_watching}" + ) while True: try: @@ -118,26 +134,25 @@ def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=Fal if self._use_byte_stream: new_output = new_output.decode() except ValueError: - OutputHandler.print(f'Value error for {pipe_name}... ') + OutputHandler.print(f"Value error for {pipe_name}... ") if self.is_finished(need_wait_output=False): break continue if write_stderr: - OutputHandler.print(f'STDERR + {len(new_output)} symbols: {new_output}') + OutputHandler.print(f"STDERR + {len(new_output)} symbols: {new_output}") if len(new_output) == 0: with self.lock: self._pipes_watching -= 1 OutputHandler.print( - f'Out of {pipe_name}... ' - f'Maybe program terminated. Pipes watching = {self._pipes_watching}' + f"Out of {pipe_name}... " + f"Maybe program terminated. Pipes watching = {self._pipes_watching}" ) if self._pipes_watching == 0: - OutputHandler.print( - f'Set alive = False for {pipe_name}... ') + OutputHandler.print(f"Set alive = False for {pipe_name}... ") self._alive = False self.terminate() @@ -147,7 +162,7 @@ def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=Fal if self.register_output: write_pipe.write(new_output) except ExitException: - OutputHandler.print(f'ExitException for {pipe_name}... ') + OutputHandler.print(f"ExitException for {pipe_name}... ") self._alive = False self.terminate() break @@ -158,17 +173,17 @@ def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=Fal if write_stderr: self.stderr += new_output - def check_stdout(self): + def check_stdout(self) -> None: self.check_pipe(self.process.stdout, sys.stdout, write_stdout=True) - def check_stderr(self): + def check_stderr(self) -> None: self.check_pipe(self.process.stderr, sys.stderr, write_stderr=True) - def check_cpuload(self): + def check_cpuload(self) -> None: while self._alive: try: cpu_load = self.ps.cpu_percent() - OutputHandler.print(f'Check cpuload - {cpu_load}') + OutputHandler.print(f"Check cpuload - {cpu_load}") if not self.initial_idle_wait: self.cpu_load_history.append(cpu_load) @@ -177,9 +192,9 @@ def check_cpuload(self): self.cpu_load_history.pop(0) except NoSuchProcess: - OutputHandler.print('Check cpuload finished, waiting output') + OutputHandler.print("Check cpuload finished, waiting output") self.wait_output() - OutputHandler.print('Check cpuload finished, set alive = false') + OutputHandler.print("Check cpuload finished, set alive = false") self._alive = False break @@ -191,7 +206,7 @@ def check_cpuload(self): if self.initial_idle_wait_time == 0: self.initial_idle_wait = False - def check_output(self): + def check_output(self) -> None: output_len_prev = len(self.stdout) while self._alive: @@ -200,7 +215,8 @@ def check_output(self): output_len_prev = output_len OutputHandler.print( - f'Check output diff - {diff}. Curr = {output_len}, prev = {output_len_prev}') + f"Check output diff - {diff}. Curr = {output_len}, prev = {output_len_prev}" + ) if not self.initial_idle_wait: self.output_diff_history.append(diff) @@ -219,24 +235,25 @@ def is_waiting_input(self) -> bool: return False program_not_loading_processor = ( - len(self.cpu_load_history) >= self.cpu_load_history_max and - sum(self.cpu_load_history) < 1 + len(self.cpu_load_history) >= self.cpu_load_history_max + and sum(self.cpu_load_history) < 1 ) program_not_printing_anything = ( - len(self.output_diff_history) >= self.output_diff_history_max and - sum(self.output_diff_history) == 0 + len(self.output_diff_history) >= self.output_diff_history_max + and sum(self.output_diff_history) == 0 ) return program_not_loading_processor and program_not_printing_anything - def register_input_request(self): + def register_input_request(self) -> None: if not self.is_waiting_input(): - raise RuntimeError('Program is not waiting for the input') + msg = "Program is not waiting for the input" + raise RuntimeError(msg) self.cpu_load_history = [] self.output_diff_history = [] - def is_finished(self, need_wait_output=True): + def is_finished(self, need_wait_output=True) -> bool: if not self.check_early_finish: return not self._alive @@ -244,7 +261,7 @@ def is_finished(self, need_wait_output=True): return True try: - is_running = self.ps.status() == 'running' + is_running = self.ps.status() == "running" if not is_running: self._alive = False except NoSuchProcess: @@ -257,54 +274,65 @@ def is_finished(self, need_wait_output=True): return not self._alive - def provide_input(self, stdin: str): - if self._use_byte_stream: - stdin = stdin.encode() - self.process.stdin.write(stdin) + def provide_input(self, stdin: str) -> None: + if not stdin.endswith("\n"): + stdin += "\n" + try: + if self._use_byte_stream: + stdin = stdin.encode("utf-8") + self.process.stdin.write(stdin) + self.process.stdin.flush() + else: + self.process.stdin.write(stdin) + self.process.stdin.flush() + except OSError: + # Handle pipe errors gracefully + if not self._alive: + return + raise - def terminate(self): - OutputHandler.print('Terminate called') + def terminate(self) -> None: + OutputHandler.print("Terminate called") with self.lock: - OutputHandler.print('Terminate - LOCK ACQUIRED') + OutputHandler.print("Terminate - LOCK ACQUIRED") if self.terminated: - OutputHandler.print('Terminate - finished') + OutputHandler.print("Terminate - finished") return - OutputHandler.print('Terminate - BEFORE WAIT STDERR') + OutputHandler.print("Terminate - BEFORE WAIT STDERR") self.wait_output() if self.register_io_handler: SystemHandler.uninstall_handler(self) - OutputHandler.print('Terminate - AFTER WAIT STDERR') + OutputHandler.print("Terminate - AFTER WAIT STDERR") self._alive = False - OutputHandler.print('Terminate - SELF ALIVE == FALSE') + OutputHandler.print("Terminate - SELF ALIVE == FALSE") is_exit_replaced = ExitHandler.is_replaced() if is_exit_replaced: ExitHandler.revert_exit() - OutputHandler.print('Terminate - EXIT REVERTED') + OutputHandler.print("Terminate - EXIT REVERTED") try: parent = Process(self.process.pid) - OutputHandler.print(f'Terminate - parent == {parent}') + OutputHandler.print(f"Terminate - parent == {parent}") for child in parent.children(recursive=True): - OutputHandler.print(f'Terminate - child kill {child}') + OutputHandler.print(f"Terminate - child kill {child}") child.kill() - OutputHandler.print(f'Terminate - parent kill {parent}') + OutputHandler.print(f"Terminate - parent kill {parent}") parent.kill() except NoSuchProcess: - OutputHandler.print('Terminate - NO SUCH PROCESS') - pass + OutputHandler.print("Terminate - NO SUCH PROCESS") finally: - OutputHandler.print('Terminate - finally before kill') + OutputHandler.print("Terminate - finally before kill") self.process.kill() - OutputHandler.print('Terminate - finally before wait') + OutputHandler.print("Terminate - finally before wait") self.process.wait() self.process.stdout.close() @@ -313,13 +341,13 @@ def terminate(self): if is_exit_replaced: ExitHandler.replace_exit() - OutputHandler.print('Terminate - EXIT REPLACED AGAIN') + OutputHandler.print("Terminate - EXIT REPLACED AGAIN") self.terminated = True - OutputHandler.print('Terminate - TERMINATED') - OutputHandler.print('Terminate - finished') + OutputHandler.print("Terminate - TERMINATED") + OutputHandler.print("Terminate - finished") - def wait_output(self): + def wait_output(self) -> None: iterations = 50 sleep_time = 50 / 1000 @@ -333,14 +361,12 @@ def wait_output(self): curr_stdout = self.stdout iterations -= 1 - def wait(self): + def wait(self) -> None: while not self.is_finished(): sleep(0.01) self.wait_output() def is_error_happened(self) -> bool: return ( - not self._alive and len(self.stderr) > 0 and - self.process.returncode != 0 or - 'Traceback' in self.stderr - ) + not self._alive and len(self.stderr) > 0 and self.process.returncode != 0 + ) or "Traceback" in self.stderr diff --git a/hstest/testing/runner/__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..974a910d 100644 --- a/hstest/testing/runner/async_dynamic_testing_runner.py +++ b/hstest/testing/runner/async_dynamic_testing_runner.py @@ -1,28 +1,30 @@ +from __future__ import annotations + import typing from concurrent.futures import Future, TimeoutError -from typing import Optional, Type from hstest.common.process_utils import DaemonThreadPoolExecutor from hstest.dynamic.output.output_handler import OutputHandler from hstest.exception.testing import ( - TestedProgramFinishedEarly, TestedProgramThrewException, TimeLimitException + TestedProgramFinishedEarly, + TestedProgramThrewException, + TimeLimitException, ) from hstest.exceptions import TestPassed, WrongAnswer from hstest.test_case.check_result import CheckResult, correct, wrong from hstest.testing.execution.main_module_executor import MainModuleExecutor -from hstest.testing.execution.program_executor import ProgramExecutor from hstest.testing.execution_options import debug_mode from hstest.testing.runner.test_runner import TestRunner -from hstest.testing.test_run import TestRun if typing.TYPE_CHECKING: from hstest import TestCase + from hstest.testing.execution.program_executor import ProgramExecutor + from hstest.testing.test_run import TestRun class AsyncDynamicTestingRunner(TestRunner): - - def __init__(self, executor: Type[ProgramExecutor] = MainModuleExecutor): - self.executor: Type[ProgramExecutor] = executor + def __init__(self, executor: type[ProgramExecutor] = MainModuleExecutor) -> None: + self.executor: type[ProgramExecutor] = executor def _run_dynamic_test(self, test_run: TestRun) -> CheckResult: test_case = test_run.test_case @@ -40,7 +42,7 @@ def _run_dynamic_test(self, test_run: TestRun) -> CheckResult: return result - def _run_file(self, test_run: TestRun) -> Optional[CheckResult]: + def _run_file(self, test_run: TestRun) -> CheckResult | None: test_case = test_run.test_case time_limit = test_case.time_limit @@ -49,8 +51,7 @@ def _run_file(self, test_run: TestRun) -> Optional[CheckResult]: future: Future = executor.submit(lambda: self._run_dynamic_test(test_run)) if time_limit <= 0 or debug_mode: return future.result() - else: - return future.result(timeout=time_limit / 1000) + return future.result(timeout=time_limit / 1000) except TimeoutError: test_run.set_error_in_test(TimeLimitException(time_limit)) except BaseException as ex: @@ -61,7 +62,7 @@ def _run_file(self, test_run: TestRun) -> Optional[CheckResult]: return None - def test(self, test_run: TestRun) -> Optional[CheckResult]: + def test(self, test_run: TestRun) -> CheckResult | None: test_case = test_run.test_case result: CheckResult = self._run_file(test_run) @@ -71,8 +72,7 @@ def test(self, test_run: TestRun) -> Optional[CheckResult]: if error is None: try: - return test_case.check_func( - OutputHandler.get_output(), test_case.attach) + return test_case.check_func(OutputHandler.get_output(), test_case.attach) except BaseException as ex: error = ex test_run.set_error_in_test(error) @@ -81,7 +81,8 @@ def test(self, test_run: TestRun) -> Optional[CheckResult]: return result - def tear_down(self, test_case: 'TestCase'): + def tear_down(self, test_case: TestCase) -> None: from hstest import StageTest + for program in StageTest.curr_test_run.tested_programs: program.executor.tear_down() diff --git a/hstest/testing/runner/django_application_runner.py b/hstest/testing/runner/django_application_runner.py index 6de0259a..6e7529cd 100644 --- a/hstest/testing/runner/django_application_runner.py +++ b/hstest/testing/runner/django_application_runner.py @@ -1,44 +1,49 @@ +from __future__ import annotations + import os import sys from time import sleep -from typing import List, Optional +from typing import TYPE_CHECKING from hstest.common.file_utils import safe_delete from hstest.common.process_utils import is_port_in_use from hstest.exception.outcomes import ErrorWithFeedback, ExceptionWithFeedback, UnexpectedError from hstest.test_case.attach.django_settings import DjangoSettings from hstest.test_case.check_result import CheckResult -from hstest.test_case.test_case import TestCase from hstest.testing.execution.filtering.file_filter import FileFilter from hstest.testing.execution.searcher.python_searcher import PythonSearcher from hstest.testing.process_wrapper import ProcessWrapper from hstest.testing.runner.test_runner import TestRunner -from hstest.testing.test_run import TestRun + +if TYPE_CHECKING: + from hstest.test_case.test_case import TestCase + from hstest.testing.test_run import TestRun class DjangoApplicationRunner(TestRunner): process: ProcessWrapper = None - port: Optional[int] = None - full_path: Optional[str] = None + port: int | None = None + full_path: str | None = None - def launch_django_application(self, test_case: TestCase): + def launch_django_application(self, test_case: TestCase) -> None: if not isinstance(test_case.attach, DjangoSettings): - raise UnexpectedError( - f'Django tests should have DjangoSettings class as an attach, ' - f'found {type(test_case.attach)}') + msg = ( + f"Django tests should have DjangoSettings class as an attach, " + f"found {type(test_case.attach)}" + ) + raise UnexpectedError(msg) source = test_case.source_name if source is None or not len(source): - source = 'manage' + source = "manage" - full_source = source.replace('.', os.sep) + '.py' + full_source = source.replace(".", os.sep) + ".py" full_path = os.path.abspath(full_source) if not os.path.exists(full_path): filename = os.path.basename(full_source) - runnable = PythonSearcher().search( - file_filter=FileFilter(file=lambda f: f == filename)) + runnable = PythonSearcher().search(file_filter=FileFilter(file=lambda f: f == filename)) full_path = os.path.abspath(runnable.folder + os.sep + runnable.file) self.full_path = full_path @@ -48,11 +53,16 @@ def launch_django_application(self, test_case: TestCase): self.__prepare_database(test_case.attach.test_database) self.process = ProcessWrapper( - sys.executable, self.full_path, 'runserver', self.port, '--noreload', - register_io_handler=True).start() + sys.executable, + self.full_path, + "runserver", + self.port, + "--noreload", + register_io_handler=True, + ).start() i: int = 100 - search_phrase = 'Starting development server at' + search_phrase = "Starting development server at" while i: if search_phrase in self.process.stdout: test_case.attach.port = self.port @@ -68,30 +78,32 @@ def launch_django_application(self, test_case: TestCase): stderr = self.process.stderr.strip() error_info = ( - f'Cannot start Django server because cannot find ' + f"Cannot start Django server because cannot find " f'"{search_phrase}" in process\' output' ) if len(stdout): - error_info += '\n\nstdout:\n' + stdout + error_info += "\n\nstdout:\n" + stdout if len(stderr): - error_info += '\n\nstderr:\n' + stderr + error_info += "\n\nstderr:\n" + stderr raise ErrorWithFeedback(error_info) - def __find_free_port(self, ports: List[int]) -> int: + def __find_free_port(self, ports: list[int]) -> int: for port in ports: if not is_port_in_use(port): return port - raise ErrorWithFeedback( - 'Cannot find a port to start Django application ' - f'(tried ports form {ports[0]} to {ports[-1]})') - - def __prepare_database(self, test_database: str): - os.environ['HYPERSKILL_TEST_DATABASE'] = test_database - with open(test_database, 'w'): + msg = ( + "Cannot find a port to start Django application " + f"(tried ports form {ports[0]} to {ports[-1]})" + ) + raise ErrorWithFeedback(msg) + + def __prepare_database(self, test_database: str) -> None: + os.environ["HYPERSKILL_TEST_DATABASE"] = test_database + with open(test_database, "w", encoding="utf-8"): pass - migrate = ProcessWrapper(sys.executable, self.full_path, 'migrate', check_early_finish=True) + migrate = ProcessWrapper(sys.executable, self.full_path, "migrate", check_early_finish=True) migrate.start() while not migrate.is_finished() and len(migrate.stderr) == 0: @@ -100,18 +112,21 @@ def __prepare_database(self, test_database: str): if len(migrate.stderr) != 0: migrate.wait_output() - if ('ModuleNotFoundError' in migrate.stderr or - 'ImportError' in migrate.stderr or - 'SyntaxError' in migrate.stderr): + if ( + "ModuleNotFoundError" in migrate.stderr + or "ImportError" in migrate.stderr + or "SyntaxError" in migrate.stderr + ): raise ExceptionWithFeedback(migrate.stderr, None) # stdout and stderr is collected and will be shown to the user - raise ErrorWithFeedback('Cannot apply migrations to an empty database.') + msg = "Cannot apply migrations to an empty database." + raise ErrorWithFeedback(msg) - def set_up(self, test_case: TestCase): + def set_up(self, test_case: TestCase) -> None: self.launch_django_application(test_case) - def tear_down(self, test_case: TestCase): + def tear_down(self, test_case: TestCase) -> None: self._check_errors() if isinstance(test_case.attach, DjangoSettings): @@ -119,12 +134,12 @@ def tear_down(self, test_case: TestCase): if self.process: self.process.terminate() - def _check_errors(self): + def _check_errors(self) -> None: if self.process.is_error_happened(): self.process.terminate() raise ErrorWithFeedback(self.process.stderr) - def test(self, test_run: TestRun) -> Optional[CheckResult]: + def test(self, test_run: TestRun) -> CheckResult | None: self._check_errors() test_case = test_run.test_case diff --git a/hstest/testing/runner/flask_application_runner.py b/hstest/testing/runner/flask_application_runner.py index 0a3391ff..5b870bf4 100644 --- a/hstest/testing/runner/flask_application_runner.py +++ b/hstest/testing/runner/flask_application_runner.py @@ -1,56 +1,65 @@ +from __future__ import annotations + import os import sys from time import sleep -from typing import List, Optional, Tuple +from typing import TYPE_CHECKING from hstest.common.process_utils import is_port_in_use from hstest.exception.outcomes import ErrorWithFeedback, UnexpectedError from hstest.test_case.attach.flask_settings import FlaskSettings from hstest.test_case.check_result import CheckResult -from hstest.test_case.test_case import TestCase from hstest.testing.process_wrapper import ProcessWrapper from hstest.testing.runner.test_runner import TestRunner -from hstest.testing.test_run import TestRun + +if TYPE_CHECKING: + from hstest.test_case.test_case import TestCase + from hstest.testing.test_run import TestRun class FlaskApplicationRunner(TestRunner): - processes: List[Tuple[str, ProcessWrapper]] = [] + processes: list[tuple[str, ProcessWrapper]] = [] - def launch_flask_applications(self, test_case: TestCase): + def launch_flask_applications(self, test_case: TestCase) -> None: if not isinstance(test_case.attach, FlaskSettings): - raise UnexpectedError( - f'Flask tests should have FlaskSettings class as an attach, ' - f'found {type(test_case.attach)}') + msg = ( + f"Flask tests should have FlaskSettings class as an attach, " + f"found {type(test_case.attach)}" + ) + raise UnexpectedError(msg) sources = test_case.attach.sources if len(sources) == 0: - raise UnexpectedError( - 'Cannot find Flask applications to run, no sources were defined in tests') + msg = "Cannot find Flask applications to run, no sources were defined in tests" + raise UnexpectedError(msg) new_sources = [] for source in sources: filename, port = source - full_source = filename.replace('.', os.sep) + '.py' + full_source = filename.replace(".", os.sep) + ".py" full_path = os.path.abspath(full_source) if not os.path.exists(full_path): - raise ErrorWithFeedback( + msg = ( f'Cannot find file named "{os.path.basename(full_path)}" ' f'in folder "{os.path.dirname(full_path)}". ' - f'Check if you deleted it.') + f"Check if you deleted it." + ) + raise ErrorWithFeedback(msg) if port is None: port = self.__find_free_port(test_case.attach.tryout_ports) process = ProcessWrapper( - sys.executable, full_path, f'localhost:{port}', register_io_handler=True).start() + sys.executable, full_path, f"localhost:{port}", register_io_handler=True + ).start() self.processes += [(full_source, process)] i: int = 100 - search_phrase = 'Press CTRL+C to quit' + search_phrase = "Press CTRL+C to quit" while i: if search_phrase in process.stderr: break @@ -65,14 +74,14 @@ def launch_flask_applications(self, test_case: TestCase): stderr = process.stderr.strip() error_info = ( - f'Cannot start Flask server {full_source} ' + f"Cannot start Flask server {full_source} " f'because cannot find "{search_phrase}" in process\' output' ) if len(stdout): - error_info += '\n\nstdout:\n' + stdout + error_info += "\n\nstdout:\n" + stdout if len(stderr): - error_info += '\n\nstderr:\n' + stderr + error_info += "\n\nstderr:\n" + stderr raise ErrorWithFeedback(error_info) @@ -80,29 +89,32 @@ def launch_flask_applications(self, test_case: TestCase): test_case.attach.sources = new_sources - def __find_free_port(self, ports: List[int]) -> int: + def __find_free_port(self, ports: list[int]) -> int: for port in ports: if not is_port_in_use(port): return port - raise ErrorWithFeedback( - 'Cannot find a port to start Flask application ' - f'(tried ports form {ports[0]} to {ports[-1]})') + msg = ( + "Cannot find a port to start Flask application " + f"(tried ports form {ports[0]} to {ports[-1]})" + ) + raise ErrorWithFeedback(msg) - def set_up(self, test_case: TestCase): + def set_up(self, test_case: TestCase) -> None: self.launch_flask_applications(test_case) - def tear_down(self, test_case: TestCase): + def tear_down(self, test_case: TestCase) -> None: for process_item in self.processes: - filename, process = process_item + _filename, process = process_item process.terminate() - def _check_errors(self): + def _check_errors(self) -> None: for process_item in self.processes: filename, process = process_item if process.is_error_happened(): - raise ErrorWithFeedback(f'Error running "{filename}"\n\n{process.stderr}') + msg = f'Error running "{filename}"\n\n{process.stderr}' + raise ErrorWithFeedback(msg) - def test(self, test_run: TestRun) -> Optional[CheckResult]: + def test(self, test_run: TestRun) -> CheckResult | None: self._check_errors() test_case = test_run.test_case diff --git a/hstest/testing/runner/plot_testing_runner.py b/hstest/testing/runner/plot_testing_runner.py index a4663dc1..b3856be7 100644 --- a/hstest/testing/runner/plot_testing_runner.py +++ b/hstest/testing/runner/plot_testing_runner.py @@ -1,6 +1,7 @@ -from typing import List, TYPE_CHECKING +from __future__ import annotations + +from typing import TYPE_CHECKING -from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.matplotlib_handler import MatplotlibHandler from hstest.testing.plotting.pandas_handler import PandasHandler from hstest.testing.plotting.seaborn_handler import SeabornHandler @@ -8,47 +9,42 @@ if TYPE_CHECKING: from hstest import TestCase + from hstest.testing.plotting.drawing.drawing import Drawing class DrawingsStorage: - def __init__(self, - all_drawings: List[Drawing], - new_drawings: List[Drawing]): - self.all_drawings: List[Drawing] = all_drawings - self.new_drawings: List[Drawing] = new_drawings + def __init__(self, all_drawings: list[Drawing], new_drawings: list[Drawing]) -> None: + self.all_drawings: list[Drawing] = all_drawings + self.new_drawings: list[Drawing] = new_drawings - def append(self, drawing: Drawing): + def append(self, drawing: Drawing) -> None: self.all_drawings.append(drawing) self.new_drawings.append(drawing) - def extend(self, drawings: List[Drawing]): + def extend(self, drawings: list[Drawing]) -> None: self.all_drawings.extend(drawings) self.new_drawings.extend(drawings) class PlottingTestingRunner(AsyncDynamicTestingRunner): - - def __init__(self, - all_drawings: List[Drawing], - new_drawings: List[Drawing]): + def __init__(self, all_drawings: list[Drawing], new_drawings: list[Drawing]) -> None: super().__init__() - self.drawings_storage: DrawingsStorage = DrawingsStorage( - all_drawings, new_drawings) + self.drawings_storage: DrawingsStorage = DrawingsStorage(all_drawings, new_drawings) - def set_up(self, test_case: 'TestCase'): + def set_up(self, test_case: TestCase) -> None: super().set_up(test_case) self.replace_plots() - def tear_down(self, test_case: 'TestCase'): + def tear_down(self, test_case: TestCase) -> None: super().tear_down(test_case) self.revert_plots() - def replace_plots(self): + def replace_plots(self) -> None: MatplotlibHandler.replace_plots(self.drawings_storage) PandasHandler.replace_plots(self.drawings_storage) SeabornHandler.replace_plots(self.drawings_storage) - def revert_plots(self): + def revert_plots(self) -> None: MatplotlibHandler.revert_plots() PandasHandler.revert_plots() SeabornHandler.revert_plots() diff --git a/hstest/testing/runner/sql_runner.py b/hstest/testing/runner/sql_runner.py index 7cce21df..0fb928b6 100644 --- a/hstest/testing/runner/sql_runner.py +++ b/hstest/testing/runner/sql_runner.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +import contextlib import os import re import sqlite3 @@ -13,53 +16,51 @@ class SQLRunner(TestRunner): - - def __init__(self, sql_test_cls): + def __init__(self, sql_test_cls) -> None: self.sql_test_cls = sql_test_cls - super(SQLRunner, self).__init__() + super().__init__() - def test(self, test_run: 'TestRun'): + def test(self, test_run: TestRun): test_case = test_run.test_case try: - result = test_case.dynamic_testing() - return result + return test_case.dynamic_testing() except BaseException as ex: test_run.set_error_in_test(ex) return CheckResult.from_error(test_run.error_in_test) - def set_up(self, test_case: 'TestCase'): + def set_up(self, test_case: TestCase) -> None: self.parse_sql_file() self.set_up_database() - def set_up_database(self): + def set_up_database(self) -> None: if self.sql_test_cls.db is not None: return - self.sql_test_cls.db = sqlite3.connect(':memory:') + self.sql_test_cls.db = sqlite3.connect(":memory:") def parse_sql_file(self) -> None: sql_file = SQLSearcher().search() file_path = os.path.join(sql_file.folder, sql_file.file) - with open(file_path, 'r') as file: + with open(file_path, encoding="utf-8") as file: lines = file.readlines() sql_content = " ".join(lines).replace("\n", "") commands = re.findall(r'(\w+)\s+?=\s+?"(.*?)"', sql_content) - for (name, query) in commands: + for name, query in commands: if not query: - raise WrongAnswer(f"The '{name}' query shouldn't be empty!") + msg = f"The '{name}' query shouldn't be empty!" + raise WrongAnswer(msg) if name in self.sql_test_cls.queries: self.sql_test_cls.queries[name] = query for name in self.sql_test_cls.queries: if self.sql_test_cls.queries[name] is None: - raise WrongAnswer(f"Can't find '{name}' query from SQL files!") + msg = f"Can't find '{name}' query from SQL files!" + raise WrongAnswer(msg) - def tear_down(self, test_case: 'TestCase'): - try: + def tear_down(self, test_case: TestCase) -> None: + with contextlib.suppress(Exception): self.sql_test_cls.db.close() - except Exception: - pass diff --git a/hstest/testing/runner/test_runner.py b/hstest/testing/runner/test_runner.py index b0d70d75..05decb78 100644 --- a/hstest/testing/runner/test_runner.py +++ b/hstest/testing/runner/test_runner.py @@ -1,19 +1,20 @@ -import typing -from typing import Optional +from __future__ import annotations -from hstest.check_result import CheckResult +import typing if typing.TYPE_CHECKING: + from hstest.check_result import CheckResult from hstest.test_case.test_case import TestCase from hstest.testing.test_run import TestRun class TestRunner: - def set_up(self, test_case: 'TestCase'): + def set_up(self, test_case: TestCase) -> None: pass - def tear_down(self, test_case: 'TestCase'): + def tear_down(self, test_case: TestCase) -> None: pass - def test(self, test_run: 'TestRun') -> Optional[CheckResult]: - raise NotImplementedError("Test method is not implemented") + def test(self, test_run: TestRun) -> CheckResult | None: + msg = "Test method is not implemented" + raise NotImplementedError(msg) diff --git a/hstest/testing/settings.py b/hstest/testing/settings.py index d8fac536..5ed56072 100644 --- a/hstest/testing/settings.py +++ b/hstest/testing/settings.py @@ -1,7 +1,11 @@ +from __future__ import annotations + + class Settings: do_reset_output: bool = True allow_out_of_input: bool = False catch_stderr: bool = True - def __init__(self): - raise NotImplementedError('Instances of the class Settings are prohibited') + def __init__(self) -> None: + msg = "Instances of the class Settings are prohibited" + raise NotImplementedError(msg) diff --git a/hstest/testing/state_machine.py b/hstest/testing/state_machine.py index 8c013a8c..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..fd88af5a 100644 --- a/hstest/testing/tested_program.py +++ b/hstest/testing/tested_program.py @@ -1,27 +1,34 @@ -from typing import List, Optional, Type +from __future__ import annotations + +from typing import TYPE_CHECKING from hstest.exception.outcomes import UnexpectedError -from hstest.testing.execution.program_executor import ProgramExecutor + +if TYPE_CHECKING: + from hstest.testing.execution.program_executor import ProgramExecutor class TestedProgram: - def __init__(self, source: str = None): + def __init__(self, source: str | None = None) -> None: from hstest import StageTest + runner = StageTest.curr_test_run.test_runner from hstest.testing.runner.async_dynamic_testing_runner import AsyncDynamicTestingRunner + if not isinstance(runner, AsyncDynamicTestingRunner): raise UnexpectedError( - 'TestedProgram is supported only while using AsyncDynamicTestingRunner runner, ' - 'not ' + str(type(runner)) + "TestedProgram is supported only while using AsyncDynamicTestingRunner runner, " + "not " + str(type(runner)) ) if source is None: from hstest.stage_test import StageTest + source = StageTest.curr_test_run.test_case.source_name self._program_executor: ProgramExecutor = runner.executor(source) - self._run_args: Optional[List[str]] = None + self._run_args: list[str] | None = None @property def run_args(self): @@ -31,17 +38,19 @@ def run_args(self): def executor(self): return self._program_executor - def _init_program(self, *args: str): + def _init_program(self, *args: str) -> None: self._run_args = args from hstest.stage_test import StageTest + if StageTest.curr_test_run: StageTest.curr_test_run.add_tested_program(self) - def feedback_on_exception(self, ex: Type[Exception], feedback: str): + def feedback_on_exception(self, ex: type[Exception], feedback: str) -> None: from hstest import StageTest + StageTest.curr_test_run.test_case.feedback_on_exception[ex] = feedback - def start_in_background(self, *args: str): + def start_in_background(self, *args: str) -> None: self._init_program(*args) self._program_executor.start_in_background(*args) @@ -49,22 +58,22 @@ def start(self, *args: str) -> str: self._init_program(*args) return self._program_executor.start(*args) - def execute(self, stdin: Optional[str]) -> str: + def execute(self, stdin: str | None) -> str: return self._program_executor.execute(stdin) def get_output(self) -> str: return self._program_executor.get_output() - def stop(self): + def stop(self) -> None: self._program_executor.stop() def is_finished(self) -> bool: return self._program_executor.is_finished() - def set_return_output_after_execution(self, value: bool): + def set_return_output_after_execution(self, value: bool) -> None: self._program_executor.set_return_output_after_execution(value) - def stop_input(self): + def stop_input(self) -> None: self._program_executor.stop_input() def is_input_allowed(self) -> bool: @@ -73,10 +82,10 @@ def is_input_allowed(self) -> bool: def is_waiting_input(self) -> bool: return self._program_executor.is_waiting_input() - def go_background(self): + def go_background(self) -> None: self._program_executor.go_background() - def stop_background(self): + def stop_background(self) -> None: self._program_executor.stop_background() def is_in_background(self) -> bool: diff --git a/hstest/testing/unittest/__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..5d641ee8 100644 --- a/hstest/testing/unittest/expected_fail_test.py +++ b/hstest/testing/unittest/expected_fail_test.py @@ -1,21 +1,28 @@ +from __future__ import annotations + +import re from inspect import cleandoc -from typing import List, Union -from hstest import StageTest +from hstest.stage.stage_test import StageTest class ExpectedFailTest(StageTest): - _base_contain: Union[str, List[str]] = [] - _base_not_contain: Union[str, List[str]] = [] + _base_contain: str | list[str] = [] + _base_not_contain: str | list[str] = [] - contain: Union[str, List[str]] = [] - not_contain: Union[str, List[str]] = [] + contain: str | list[str] = [] + not_contain: str | list[str] = [] - def __init__(self, args): + def __init__(self, args) -> None: super().__init__(args) - def test_run_unittest(self): + def normalize_error_message(self, message: str) -> str: + # Remove error pointer markers added in Python 3.11+ + message = re.sub(r"\s+[~^]+\s*", " ", message) + # Normalize whitespace and line breaks + return " ".join(message.split()) + def test_run_unittest(self) -> None: if not self.contain and not self.not_contain: self.fail("'contain' or 'not_contain' should not be empty") @@ -24,19 +31,31 @@ def test_run_unittest(self): self.assertEqual(result, -1) if type(self._base_contain) != list: - self._base_contain = [self._base_contain] + self._base_contain = [self._base_contain] # type: ignore[list-item] if type(self._base_not_contain) != list: - self._base_not_contain = [self._base_not_contain] + self._base_not_contain = [self._base_not_contain] # type: ignore[list-item] if type(self.contain) != list: - self.contain = [self.contain] + self.contain = [self.contain] # type: ignore[list-item] if type(self.not_contain) != list: - self.not_contain = [self.not_contain] + self.not_contain = [self.not_contain] # type: ignore[list-item] should_contain = self._base_contain + self.contain should_not_contain = self._base_not_contain + self.not_contain + normalized_feedback = self.normalize_error_message(feedback) + for item in should_contain: - self.assertIn(cleandoc(item), feedback) + normalized_item = self.normalize_error_message(cleandoc(item)) + self.assertIn( + normalized_item, + normalized_feedback, + f"Expected to find:\n{normalized_item}\nin:\n{normalized_feedback}", + ) for item in should_not_contain: - self.assertNotIn(cleandoc(item), feedback) + normalized_item = self.normalize_error_message(cleandoc(item)) + self.assertNotIn( + normalized_item, + normalized_feedback, + f"Expected NOT to find:\n{normalized_item}\nin:\n{normalized_feedback}", + ) diff --git a/hstest/testing/unittest/unexepected_error_test.py b/hstest/testing/unittest/unexepected_error_test.py index 36492d4f..3433da0c 100644 --- a/hstest/testing/unittest/unexepected_error_test.py +++ b/hstest/testing/unittest/unexepected_error_test.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from hstest.testing.unittest.expected_fail_test import ExpectedFailTest class UnexpectedErrorTest(ExpectedFailTest): - _base_contain = 'Unexpected error' + _base_contain = "Unexpected error" diff --git a/hstest/testing/unittest/user_error_test.py b/hstest/testing/unittest/user_error_test.py index 1f7b4d9f..71310f1c 100644 --- a/hstest/testing/unittest/user_error_test.py +++ b/hstest/testing/unittest/user_error_test.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from hstest.testing.unittest.expected_fail_test import ExpectedFailTest class UserErrorTest(ExpectedFailTest): - _base_not_contain = 'Unexpected error' + _base_not_contain = "Unexpected error" diff --git a/poetry.lock b/poetry.lock new file mode 100755 index 00000000..699e3883 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1000 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" and sys_platform == \"win32\" or python_version >= \"3.12\" and sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "contourpy" +version = "1.3.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"}, + {file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3"}, + {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277"}, + {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595"}, + {file = "contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697"}, + {file = "contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e"}, + {file = "contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b"}, + {file = "contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c"}, + {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291"}, + {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f"}, + {file = "contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"}, + {file = "contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9"}, + {file = "contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509"}, + {file = "contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9"}, + {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b"}, + {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d"}, + {file = "contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e"}, + {file = "contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d"}, + {file = "contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2"}, + {file = "contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c"}, + {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3"}, + {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1"}, + {file = "contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82"}, + {file = "contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd"}, + {file = "contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30"}, + {file = "contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda"}, + {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242"}, + {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1"}, + {file = "contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1"}, + {file = "contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53"}, + {file = "contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699"}, +] + +[package.dependencies] +numpy = ">=1.23" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fonttools" +version = "4.56.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000"}, + {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16"}, + {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311"}, + {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc"}, + {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f"}, + {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086"}, + {file = "fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786"}, + {file = "fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685"}, + {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df"}, + {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c"}, + {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c"}, + {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049"}, + {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62"}, + {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0"}, + {file = "fonttools-4.56.0-cp311-cp311-win32.whl", hash = "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b"}, + {file = "fonttools-4.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05"}, + {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9"}, + {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f"}, + {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2"}, + {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563"}, + {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a"}, + {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28"}, + {file = "fonttools-4.56.0-cp312-cp312-win32.whl", hash = "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c"}, + {file = "fonttools-4.56.0-cp312-cp312-win_amd64.whl", hash = "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba"}, + {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692"}, + {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0"}, + {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1"}, + {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea"}, + {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3"}, + {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278"}, + {file = "fonttools-4.56.0-cp313-cp313-win32.whl", hash = "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f"}, + {file = "fonttools-4.56.0-cp313-cp313-win_amd64.whl", hash = "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6"}, + {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3fd3fccb7b9adaaecfa79ad51b759f2123e1aba97f857936ce044d4f029abd71"}, + {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193b86e9f769320bc98ffdb42accafb5d0c8c49bd62884f1c0702bc598b3f0a2"}, + {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e81c1cc80c1d8bf071356cc3e0e25071fbba1c75afc48d41b26048980b3c771"}, + {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9270505a19361e81eecdbc2c251ad1e1a9a9c2ad75fa022ccdee533f55535dc"}, + {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53f5e9767978a4daf46f28e09dbeb7d010319924ae622f7b56174b777258e5ba"}, + {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9da650cb29bc098b8cfd15ef09009c914b35c7986c8fa9f08b51108b7bc393b4"}, + {file = "fonttools-4.56.0-cp38-cp38-win32.whl", hash = "sha256:965d0209e6dbdb9416100123b6709cb13f5232e2d52d17ed37f9df0cc31e2b35"}, + {file = "fonttools-4.56.0-cp38-cp38-win_amd64.whl", hash = "sha256:654ac4583e2d7c62aebc6fc6a4c6736f078f50300e18aa105d87ce8925cfac31"}, + {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca7962e8e5fc047cc4e59389959843aafbf7445b6c08c20d883e60ced46370a5"}, + {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1af375734018951c31c0737d04a9d5fd0a353a0253db5fbed2ccd44eac62d8c"}, + {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:442ad4122468d0e47d83bc59d0e91b474593a8c813839e1872e47c7a0cb53b10"}, + {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf4f8d2a30b454ac682e12c61831dcb174950c406011418e739de592bbf8f76"}, + {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96a4271f63a615bcb902b9f56de00ea225d6896052c49f20d0c91e9f43529a29"}, + {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d38642ca2dddc7ae992ef5d026e5061a84f10ff2b906be5680ab089f55bb8"}, + {file = "fonttools-4.56.0-cp39-cp39-win32.whl", hash = "sha256:2d351275f73ebdd81dd5b09a8b8dac7a30f29a279d41e1c1192aedf1b6dced40"}, + {file = "fonttools-4.56.0-cp39-cp39-win_amd64.whl", hash = "sha256:d6ca96d1b61a707ba01a43318c9c40aaf11a5a568d1e61146fafa6ab20890793"}, + {file = "fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14"}, + {file = "fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e"}, + {file = "kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751"}, + {file = "kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67"}, + {file = "kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34"}, + {file = "kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8"}, + {file = "kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50"}, + {file = "kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb"}, + {file = "kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2"}, + {file = "kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b"}, + {file = "kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e"}, +] + +[[package]] +name = "matplotlib" +version = "3.10.0" +description = "Python plotting package" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"}, + {file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"}, + {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5"}, + {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6"}, + {file = "matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1"}, + {file = "matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3"}, + {file = "matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"}, + {file = "matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997"}, + {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef"}, + {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683"}, + {file = "matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765"}, + {file = "matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a"}, + {file = "matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59"}, + {file = "matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a"}, + {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95"}, + {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8"}, + {file = "matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12"}, + {file = "matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc"}, + {file = "matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25"}, + {file = "matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908"}, + {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2"}, + {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf"}, + {file = "matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae"}, + {file = "matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442"}, + {file = "matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06"}, + {file = "matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff"}, + {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593"}, + {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e"}, + {file = "matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede"}, + {file = "matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef"}, + {file = "matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "numpy" +version = "2.2.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"}, + {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"}, + {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"}, + {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"}, + {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"}, + {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"}, + {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"}, + {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"}, + {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"}, + {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"}, + {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"}, + {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pandas" +version = "2.2.3" +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.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pillow" +version = "11.1.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, + {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, + {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, + {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, + {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, + {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, + {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, + {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, + {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, + {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, + {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, + {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, + {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, + {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, + {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, + {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, + {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, + {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, + {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, + {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, + {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "psutil" +version = "7.0.0" +description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, + {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, + {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, + {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, + {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, + {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, +] + +[package.extras] +dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "pyparsing" +version = "3.2.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2025.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, +] + +[[package]] +name = "ruff" +version = "0.7.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"}, + {file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"}, + {file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"}, + {file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"}, + {file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"}, + {file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"}, + {file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"}, + {file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"}, + {file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"}, + {file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"}, + {file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"}, +] + +[[package]] +name = "scipy" +version = "1.15.3" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}, + {file = "scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}, + {file = "scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"}, + {file = "scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"}, + {file = "scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"}, + {file = "scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"}, + {file = "scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.5" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "seaborn" +version = "0.13.2" +description = "Statistical data visualization" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"}, + {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"}, +] + +[package.dependencies] +matplotlib = ">=3.4,<3.6.1 || >3.6.1" +numpy = ">=1.20,<1.24.0 || >1.24.0" +pandas = ">=1.2" + +[package.extras] +dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"] +docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] +stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2025.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, + {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "caddf5b2fa311dc7112a4da8bd42fc71cdf88507c10839441225082b343df16c" diff --git a/pyproject.toml b/pyproject.toml old mode 100644 new mode 100755 index adbf17d5..a4af8fb4 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,160 @@ -[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 = "11.0.27" +description = "" +authors = ["Hyperskill Team"] +readme = "README.md" +packages = [ + { include = "hstest" }, +] + +[tool.poetry.dependencies] +python = "^3.10" + +# psutil with platform and Python version markers +psutil = "7.0.0" +pandas = "2.2.3" +seaborn = "0.13.2" +scipy = "1.15.3" +matplotlib = "^3.9.2" + +[tool.poetry.group.dev.dependencies] +mypy = "1.13.0" +ruff = "0.7.3" +pytest = "8.3.4" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 100 +target-version = "py310" +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "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"] +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..d524b1db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,7 @@ -psutil-wheels ; python_version >= '3.10' -psutil ; python_version < '3.10' +psutil==7.0.0 +mypy==1.13.0 +pandas==2.2.3 +ruff==0.7.3 +matplotlib==3.9.2 +seaborn==0.13.2 +scipy==1.15.3 \ No newline at end of file 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/outcomes/dynamic_method/test_dynamic_method_exception/test.py b/tests/outcomes/dynamic_method/test_dynamic_method_exception/test.py index e14d20c1..ca6c43f8 100644 --- a/tests/outcomes/dynamic_method/test_dynamic_method_exception/test.py +++ b/tests/outcomes/dynamic_method/test_dynamic_method_exception/test.py @@ -5,23 +5,26 @@ class TestDynamicMethodException(UserErrorTest): - contain = """ - Exception in test #1 + contain = [ + """ + Exception in test #1 - Traceback (most recent call last): - File "main.py", line 3, in - print(0/0) - ZeroDivisionError: division by zero - - Please find below the output of your program during this failed test. - Note that the '>' character indicates the beginning of the input line. - - --- - - Server started! - > main - S1: main - """ # noqa: W293 + Traceback (most recent call last): + File "main.py", line 3, in + print(0/0) + ~^~ + ZeroDivisionError: division by zero + + Please find below the output of your program during this failed test. + Note that the '>' character indicates the beginning of the input line. + + --- + + Server started! + > main + S1: main + """ + ] @dynamic_test def test(self): diff --git a/tests/outcomes/feedback_on_exception/feedback_on_exception_test_1/test.py b/tests/outcomes/feedback_on_exception/feedback_on_exception_test_1/test.py index 91e51218..e5171a58 100644 --- a/tests/outcomes/feedback_on_exception/feedback_on_exception_test_1/test.py +++ b/tests/outcomes/feedback_on_exception/feedback_on_exception_test_1/test.py @@ -14,6 +14,7 @@ class FeedbackOnExceptionTest1(UserErrorTest): Traceback (most recent call last): File "main.py", line 2, in print(1 / 0) + ~~^~~ ZeroDivisionError: division by zero Please find below the output of your program during this failed test. diff --git a/tests/outcomes/lib/exception_in_user_code_2/test.py b/tests/outcomes/lib/exception_in_user_code_2/test.py index b7789d55..1580fc10 100644 --- a/tests/outcomes/lib/exception_in_user_code_2/test.py +++ b/tests/outcomes/lib/exception_in_user_code_2/test.py @@ -12,8 +12,10 @@ class ExceptionInUserCodeTest(UserErrorTest): Traceback (most recent call last): File "main.py", line 3, in print(0 / 0) + ~~^~~ File "main.py", line 5, in print(0 / 0) + ~~^~~ ZeroDivisionError: division by zero Please find below the output of your program during this failed test. diff --git a/tests/outcomes/lib/exception_in_user_code_3/test.py b/tests/outcomes/lib/exception_in_user_code_3/test.py index 9410655c..a140e13e 100644 --- a/tests/outcomes/lib/exception_in_user_code_3/test.py +++ b/tests/outcomes/lib/exception_in_user_code_3/test.py @@ -6,14 +6,15 @@ class ExceptionInUserCodeTest3(UserErrorTest): - contain = """ + contain = ''' Exception in test #1 Traceback (most recent call last): File "main.py", line 2, in - Coffee is ready!\"\"\", raise_error_here) + Coffee is ready!""", raise_error_here) + ^^^^^^^^^^^^^^^^ NameError: name 'raise_error_here' is not defined - """ + ''' def generate(self) -> List[TestCase]: return [TestCase()] diff --git a/tests/outcomes/plot/bar/pandas/test_example/cleaning.py b/tests/outcomes/plot/bar/pandas/test_example/cleaning.py index f65f04ac..6f67738b 100644 --- a/tests/outcomes/plot/bar/pandas/test_example/cleaning.py +++ b/tests/outcomes/plot/bar/pandas/test_example/cleaning.py @@ -5,7 +5,7 @@ df_ab = pd.read_csv('ab_test.csv') # Plot dates -df_ab['date'] = df_ab['date'].astype('datetime64') +df_ab["date"] = pd.to_datetime(df_ab["date"]) control = df_ab[df_ab['group'] == 'Control'].groupby(df_ab['date'].dt.day).size().rename('Control') experiment = df_ab[df_ab['group'] == 'Experimental'].groupby(df_ab['date'].dt.day).size().rename('Experimental') diff --git a/tests/outcomes/plot/test_seaborn/test_revert/main.py b/tests/outcomes/plot/test_seaborn/test_revert/main.py index 0528854b..b3d92d4e 100644 --- a/tests/outcomes/plot/test_seaborn/test_revert/main.py +++ b/tests/outcomes/plot/test_seaborn/test_revert/main.py @@ -35,7 +35,7 @@ def plot(): sns.violinplot(data=df, palette="Set3", bw=.2, cut=1, linewidth=1) flights_long = sns.load_dataset("flights") - flights = flights_long.pivot("month", "year", "passengers") + flights = flights_long.pivot(index="month", columns="year", values="passengers") sns.heatmap(flights, annot=True, fmt="d", linewidths=.5, ax=ax) tips = sns.load_dataset("tips") diff --git a/tests/outcomes/syntax_error/test_empty_eval/test.py b/tests/outcomes/syntax_error/test_empty_eval/test.py index e24dce10..c6a1769c 100644 --- a/tests/outcomes/syntax_error/test_empty_eval/test.py +++ b/tests/outcomes/syntax_error/test_empty_eval/test.py @@ -12,8 +12,11 @@ class TestEmptyEval(UserErrorTest): Traceback (most recent call last): File "main.py", line 1, in print(eval("")) - File "", line 0""", - "SyntaxError: " + ^^^^^^^^ + File "", line 0 + + SyntaxError: invalid syntax + """ ] def generate(self) -> List[TestCase]: diff --git a/tests/outcomes/syntax_error/test_error_using_eval/test.py b/tests/outcomes/syntax_error/test_error_using_eval/test.py index 95cab756..477bbcbb 100644 --- a/tests/outcomes/syntax_error/test_error_using_eval/test.py +++ b/tests/outcomes/syntax_error/test_error_using_eval/test.py @@ -12,9 +12,12 @@ class TestEmptyEval(UserErrorTest): Traceback (most recent call last): File "main.py", line 1, in print(eval(")")) + ^^^^^^^^^ File "", line 1 - """, - "SyntaxError: " + ) + ^ + SyntaxError: unmatched ')' + """ ] def generate(self) -> List[TestCase]: diff --git a/tests/outcomes/syntax_error/test_error_using_eval_and_print/test.py b/tests/outcomes/syntax_error/test_error_using_eval_and_print/test.py index a34e5fe1..8d3715a4 100644 --- a/tests/outcomes/syntax_error/test_error_using_eval_and_print/test.py +++ b/tests/outcomes/syntax_error/test_error_using_eval_and_print/test.py @@ -12,12 +12,14 @@ class TestEmptyEval(UserErrorTest): Traceback (most recent call last): File "main.py", line 2, in print(eval(")")) + ^^^^^^^^^ File "", line 1 - """, - "SyntaxError: ", - """ + ) + ^ + SyntaxError: unmatched ')' + Please find below the output of your program during this failed test. - + --- 123 diff --git a/tests/projects/go/coffee_machine/stage1_ce/tests.py b/tests/projects/go/coffee_machine/stage1_ce/tests.py index 436a5500..a8489510 100644 --- a/tests/projects/go/coffee_machine/stage1_ce/tests.py +++ b/tests/projects/go/coffee_machine/stage1_ce/tests.py @@ -1,7 +1,10 @@ +from typing import List + from hstest.stage_test import * -from hstest.test_case import TestCase +from hstest.test_case import TestCase, CheckResult from hstest.testing.unittest.user_error_test import UserErrorTest import os +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) @@ -21,7 +24,7 @@ class CoffeeMachineTest(UserErrorTest): contain = f""" Compilation error - .{os.sep}main.go:4:2: imported and not used: "fmt" + .{os.sep}main.go:4:2: "fmt" imported and not used .{os.sep}main.go:8:2: undefined: Println """ diff --git a/tests/projects/go/coffee_machine/stage1_ex/tests.py b/tests/projects/go/coffee_machine/stage1_ex/tests.py index 7d81161d..cfa72239 100644 --- a/tests/projects/go/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/go/coffee_machine/stage1_ex/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * -from hstest.test_case import TestCase +from hstest.test_case import TestCase, CheckResult from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage1_wa/tests.py b/tests/projects/go/coffee_machine/stage1_wa/tests.py index f2585377..70d32f68 100644 --- a/tests/projects/go/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/go/coffee_machine/stage1_wa/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * -from hstest.test_case import TestCase +from hstest.test_case import TestCase, CheckResult from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage2/tests.py b/tests/projects/go/coffee_machine/stage2/tests.py index e2878fd7..89f36434 100644 --- a/tests/projects/go/coffee_machine/stage2/tests.py +++ b/tests/projects/go/coffee_machine/stage2/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage3/tests.py b/tests/projects/go/coffee_machine/stage3/tests.py index 3601e4c3..f96b322d 100644 --- a/tests/projects/go/coffee_machine/stage3/tests.py +++ b/tests/projects/go/coffee_machine/stage3/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage4/tests.py b/tests/projects/go/coffee_machine/stage4/tests.py index 213047e6..8d431f26 100644 --- a/tests/projects/go/coffee_machine/stage4/tests.py +++ b/tests/projects/go/coffee_machine/stage4/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage5/tests.py b/tests/projects/go/coffee_machine/stage5/tests.py index a36bc59a..77c1c85b 100644 --- a/tests/projects/go/coffee_machine/stage5/tests.py +++ b/tests/projects/go/coffee_machine/stage5/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage1/tests.py b/tests/projects/javascript/coffee_machine/stage1/tests.py index fea90ed8..947ca431 100644 --- a/tests/projects/javascript/coffee_machine/stage1/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage1_ce/tests.py b/tests/projects/javascript/coffee_machine/stage1_ce/tests.py index b455f2dd..5c8a4df2 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.test_case import TestCase, CheckResult 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..5ff02d5d 100644 --- a/tests/projects/javascript/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1_ex/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * -from hstest.test_case import TestCase +from hstest.test_case import TestCase, CheckResult from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage1_wa/tests.py b/tests/projects/javascript/coffee_machine/stage1_wa/tests.py index f2585377..70d32f68 100644 --- a/tests/projects/javascript/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1_wa/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * -from hstest.test_case import TestCase +from hstest.test_case import TestCase, CheckResult from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage2/tests.py b/tests/projects/javascript/coffee_machine/stage2/tests.py index e2878fd7..6de71aba 100644 --- a/tests/projects/javascript/coffee_machine/stage2/tests.py +++ b/tests/projects/javascript/coffee_machine/stage2/tests.py @@ -1,5 +1,8 @@ -from hstest.stage_test import * +from typing import Any, List + +from hstest.stage_test import StageTest from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage3/tests.py b/tests/projects/javascript/coffee_machine/stage3/tests.py index 3601e4c3..f96b322d 100644 --- a/tests/projects/javascript/coffee_machine/stage3/tests.py +++ b/tests/projects/javascript/coffee_machine/stage3/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage4/tests.py b/tests/projects/javascript/coffee_machine/stage4/tests.py index 213047e6..8d431f26 100644 --- a/tests/projects/javascript/coffee_machine/stage4/tests.py +++ b/tests/projects/javascript/coffee_machine/stage4/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage5/tests.py b/tests/projects/javascript/coffee_machine/stage5/tests.py index a36bc59a..77c1c85b 100644 --- a/tests/projects/javascript/coffee_machine/stage5/tests.py +++ b/tests/projects/javascript/coffee_machine/stage5/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage1/tests.py b/tests/projects/javascript/simple_chatty_bot/stage1/tests.py index da7967c9..38e97213 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage1/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage1/tests.py @@ -1,7 +1,9 @@ import re +from typing import List from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage2/tests.py b/tests/projects/javascript/simple_chatty_bot/stage2/tests.py index 82562c96..4672d935 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage2/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage2/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage3/tests.py b/tests/projects/javascript/simple_chatty_bot/stage3/tests.py index ea267bb5..5e6855b1 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage3/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage3/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage4/tests.py b/tests/projects/javascript/simple_chatty_bot/stage4/tests.py index 462b9a64..f4870050 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage4/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage4/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage5/tests.py b/tests/projects/javascript/simple_chatty_bot/stage5/tests.py index 092cffbd..7d3b7bcb 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage5/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage5/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage1/tests.py b/tests/projects/python/coffee_machine/stage1/tests.py index fea90ed8..947ca431 100644 --- a/tests/projects/python/coffee_machine/stage1/tests.py +++ b/tests/projects/python/coffee_machine/stage1/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage2/tests.py b/tests/projects/python/coffee_machine/stage2/tests.py index e2878fd7..89f36434 100644 --- a/tests/projects/python/coffee_machine/stage2/tests.py +++ b/tests/projects/python/coffee_machine/stage2/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage3/tests.py b/tests/projects/python/coffee_machine/stage3/tests.py index 3601e4c3..f96b322d 100644 --- a/tests/projects/python/coffee_machine/stage3/tests.py +++ b/tests/projects/python/coffee_machine/stage3/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage4/tests.py b/tests/projects/python/coffee_machine/stage4/tests.py index 213047e6..8d431f26 100644 --- a/tests/projects/python/coffee_machine/stage4/tests.py +++ b/tests/projects/python/coffee_machine/stage4/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage5/tests.py b/tests/projects/python/coffee_machine/stage5/tests.py index a36bc59a..77c1c85b 100644 --- a/tests/projects/python/coffee_machine/stage5/tests.py +++ b/tests/projects/python/coffee_machine/stage5/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/shell/coffee_machine/stage1/tests.py b/tests/projects/shell/coffee_machine/stage1/tests.py index 60bb2b04..e7b4ee80 100644 --- a/tests/projects/shell/coffee_machine/stage1/tests.py +++ b/tests/projects/shell/coffee_machine/stage1/tests.py @@ -1,3 +1,6 @@ +from typing import List + +from hstest.check_result import CheckResult from hstest.stage_test import * from hstest.test_case import TestCase from hstest.common.os_utils import is_windows diff --git a/tests/projects/shell/coffee_machine/stage1_ex/tests.py b/tests/projects/shell/coffee_machine/stage1_ex/tests.py index 98652a9b..75b14023 100644 --- a/tests/projects/shell/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/shell/coffee_machine/stage1_ex/tests.py @@ -1,3 +1,6 @@ +from typing import List + +from hstest.check_result import CheckResult from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest diff --git a/tests/projects/shell/coffee_machine/stage1_wa/tests.py b/tests/projects/shell/coffee_machine/stage1_wa/tests.py index 7184b6f3..41dffdae 100644 --- a/tests/projects/shell/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/shell/coffee_machine/stage1_wa/tests.py @@ -1,3 +1,6 @@ +from typing import List + +from hstest.check_result import CheckResult from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest diff --git a/tests/projects/shell/coffee_machine/stage2/tests.py b/tests/projects/shell/coffee_machine/stage2/tests.py index 06a8cbc1..5a22bcc9 100644 --- a/tests/projects/shell/coffee_machine/stage2/tests.py +++ b/tests/projects/shell/coffee_machine/stage2/tests.py @@ -1,3 +1,6 @@ +from typing import List + +from hstest.check_result import CheckResult from hstest.stage_test import * from hstest.test_case import TestCase from hstest.common.os_utils import is_windows diff --git a/tests/test_check_result.py b/tests/test_check_result.py index 3d3481d2..0f38e3ed 100644 --- a/tests/test_check_result.py +++ b/tests/test_check_result.py @@ -1,15 +1,17 @@ +from __future__ import annotations + import unittest from hstest.check_result import CheckResult class TestCheckResult(unittest.TestCase): - def test_true(self): + def test_true(self) -> None: r = CheckResult.correct() self.assertTrue(r.is_correct) - self.assertEqual(r.feedback, '') + self.assertEqual(r.feedback, "") - def test_false(self): - r = CheckResult.wrong('hello') + def test_false(self) -> None: + r = CheckResult.wrong("hello") self.assertFalse(r.is_correct) - self.assertEqual(r.feedback, 'hello') + self.assertEqual(r.feedback, "hello") diff --git a/tests/test_testcase.py b/tests/test_testcase.py index 45ebdb94..21c6a067 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"]) # type: ignore[list-item] diff --git a/tests/testing.py b/tests/testing.py index 1bfb5920..e9f02e3d 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -1,4 +1,5 @@ -import io +from __future__ import annotations + import re import sys import unittest @@ -6,41 +7,41 @@ from inspect import getmembers, isclass from os import listdir from os.path import abspath, dirname, isdir, isfile -from typing import List +from typing import TYPE_CHECKING -content_path = dirname( - dirname(abspath(__file__)) -) +content_path = dirname(dirname(abspath(__file__))) sys.path.insert(0, content_path) from hstest.common import utils as hs # noqa: E402 from hstest.dynamic.output.colored_output import GREEN_BOLD, RED_BOLD, RESET # noqa: E402 +if TYPE_CHECKING: + import io + class OutputForTest: - def __init__(self, real_out: io.TextIOWrapper): + def __init__(self, real_out: io.TextIOWrapper) -> None: self.original: io.TextIOWrapper = real_out - def write(self, text): - text = re.sub(r'(? None: + text = re.sub(r"(? None: self.original.flush() - def close(self): + def close(self) -> None: self.original.close() class UnitTesting: - @staticmethod def test_all() -> bool: old_run = unittest.TestCase.run @@ -48,33 +49,33 @@ def test_all() -> bool: def run(self, result=None, repeats=0): failures_before = 0 if result is None else len(result.failures) test_result = old_run(self, result=result) - is_project_test = 'tests.projects.' in str(self) + is_project_test = "tests.projects." in str(self) if repeats == 5: # max 5 times return test_result if is_project_test and test_result and failures_before < len(test_result.failures): - print('Rerun project test') test_result.failures.pop() return run(self, result=test_result, repeats=repeats + 1) return test_result unittest.TestCase.run = run - hs.failed_msg_start = '' - hs.failed_msg_continue = '' - hs.success_msg = '' + hs.failed_msg_start = "" + hs.failed_msg_continue = "" + hs.success_msg = "" tests_suite = [] loader = unittest.TestLoader() for module in UnitTesting.find_modules(dirname(__file__)): - if 'outcomes' in module and not module.endswith('.test') or \ - 'projects' in module and not module.endswith('.tests'): + if ("outcomes" in module and not module.endswith(".test")) or ( + "projects" in module and not module.endswith(".tests") + ): continue try: - imported = import_module(f'tests.{module}') + imported = import_module(f"tests.{module}") except ImportError: continue - for name, obj in getmembers(imported): + for _name, obj in getmembers(imported): if isclass(obj) and issubclass(obj, unittest.TestCase): tests_suite += [loader.loadTestsFromTestCase(obj)] @@ -84,8 +85,7 @@ def run(self, result=None, repeats=0): return result.wasSuccessful() @staticmethod - def find_modules(from_directory: str) -> List[str]: - + def find_modules(from_directory: str) -> list[str]: catalogs = [from_directory] curr_dir = from_directory @@ -94,17 +94,17 @@ def find_modules(from_directory: str) -> List[str]: while catalogs: curr_catalog = catalogs.pop() for file in listdir(curr_catalog): - curr_location = curr_catalog + '/' + file - if file.startswith('__'): + curr_location = curr_catalog + "/" + file + if file.startswith("__"): continue if isfile(curr_location): - if file.endswith('.py'): - modules += [curr_location[len(curr_dir) + 1:-3].replace('/', '.')] + if file.endswith(".py"): + modules += [curr_location[len(curr_dir) + 1 : -3].replace("/", ".")] elif isdir(curr_location): catalogs += [curr_location] return modules -if __name__ == '__main__': - exit(0 if UnitTesting.test_all() else -1) +if __name__ == "__main__": + sys.exit(0 if UnitTesting.test_all() else -1)