pax_global_header00006660000000000000000000000064151331715450014517gustar00rootroot0000000000000052 comment=27bc6b118269a28097519380ae4f13de6a43ad48 pyTooling-8.11.0/000077500000000000000000000000001513317154500135725ustar00rootroot00000000000000pyTooling-8.11.0/.editorconfig000066400000000000000000000006141513317154500162500ustar00rootroot00000000000000root = true [*] charset = utf-8 # end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = tab indent_size = 2 tab_width = 2 [*.py] indent_style = tab indent_size = 2 [*.{yml,yaml}] indent_style = space indent_size = 2 [*.{json,ini}] indent_style = tab indent_size = 2 [*.md] trim_trailing_whitespace = false [*.rst] indent_style = space indent_size = 3 pyTooling-8.11.0/.github/000077500000000000000000000000001513317154500151325ustar00rootroot00000000000000pyTooling-8.11.0/.github/CODEOWNERS000066400000000000000000000000411513317154500165200ustar00rootroot00000000000000* @Paebbels /.github/ @Paebbels pyTooling-8.11.0/.github/dependabot.yml000066400000000000000000000010041513317154500177550ustar00rootroot00000000000000version: 2 updates: # Maintain Python packages - package-ecosystem: "pip" directory: "/" target-branch: dev commit-message: prefix: "[Dependabot]" labels: - Dependencies schedule: interval: "daily" # Checks on Monday trough Friday. # Maintain GitHub Action runners - package-ecosystem: "github-actions" directory: "/" target-branch: dev commit-message: prefix: "[Dependabot]" labels: - Dependencies schedule: interval: "weekly" pyTooling-8.11.0/.github/pull_request_template.md000066400000000000000000000003031513317154500220670ustar00rootroot00000000000000# New Features * tbd * tbd # Changes * tbd * tbd # Bug Fixes * tbd * tbd # Documentation * tbd * tbd # Unit Tests * tbd * tbd ---------- # Related Issues and Pull-Requests * tbd * tbd pyTooling-8.11.0/.github/workflows/000077500000000000000000000000001513317154500171675ustar00rootroot00000000000000pyTooling-8.11.0/.github/workflows/Benchmark.yml000066400000000000000000000027571513317154500216170ustar00rootroot00000000000000name: Benchmark on: # push: workflow_dispatch: jobs: ConfigParams: uses: pyTooling/Actions/.github/workflows/ExtractConfiguration.yml@r7 BenchmarkingParams: uses: pyTooling/Actions/.github/workflows/Parameters.yml@r7 with: name: Benchmark python_version_list: "3.12 3.13 3.14 pypy-3.11" pipeline-delay: 240 Benchmarking: uses: pyTooling/Actions/.github/workflows/UnitTesting.yml@r7 needs: - ConfigParams - BenchmarkingParams with: jobs: ${{ needs.BenchmarkingParams.outputs.python_jobs }} requirements: '-r tests/benchmark/requirements.txt' unittest_directory: 'benchmark' unittest_report_xml: ${{ needs.ConfigParams.outputs.unittest_report_xml }} coverage_report_html: ${{ needs.ConfigParams.outputs.coverage_report_html }} unittest_xml_artifact: 'pyTooling-BenchmarkTestReportSummary-XML' PublishTestResults: uses: pyTooling/Actions/.github/workflows/PublishTestResults.yml@r7 needs: - ConfigParams - Benchmarking with: testsuite-summary-name: ${{ needs.ConfigParams.outputs.package_fullname }} unittest_artifacts_pattern: "*-BenchmarkTestReportSummary-XML-*" report_title: 'Benchmark Test Summary' merged_junit_artifact: pyTooling-BenchmarkTestReportSummary-XML dorny: true codecov: true codecov_flags: 'benchmark' secrets: inherit pyTooling-8.11.0/.github/workflows/Performance.yml000066400000000000000000000025121513317154500221530ustar00rootroot00000000000000name: Performance on: # push: workflow_dispatch: jobs: ConfigParams: uses: pyTooling/Actions/.github/workflows/ExtractConfiguration.yml@r7 PerformanceTestingParams: uses: pyTooling/Actions/.github/workflows/Parameters.yml@r7 with: name: Performance python_version_list: "3.12 3.13 3.14 pypy-3.11" system_list: "ubuntu windows macos macos-arm" pipeline-delay: 300 PerformanceTesting: uses: pyTooling/Actions/.github/workflows/UnitTesting.yml@r7 needs: - ConfigParams - PerformanceTestingParams with: jobs: ${{ needs.PerformanceTestingParams.outputs.python_jobs }} requirements: '-r tests/performance/requirements.txt' unittest_directory: 'performance' unittest_report_xml: ${{ needs.ConfigParams.outputs.unittest_report_xml }} coverage_report_html: ${{ needs.ConfigParams.outputs.coverage_report_html }} unittest_xml_artifact: 'pyTooling-PerformanceTestReportSummary-XML' # PublishTestResults: # uses: pyTooling/Actions/.github/workflows/PublishTestResults.yml@r7 # needs: ## - PerformanceTesting # with: # unittest_artifacts_pattern: "*-PerformanceTestReportSummary-XML-*" # report_title: Performance Test Summary # merged_junit_artifact: pyTooling-PerformanceTestReportSummary-XML pyTooling-8.11.0/.github/workflows/Pipeline.yml000066400000000000000000000314321513317154500214620ustar00rootroot00000000000000name: Pipeline on: push: workflow_dispatch: schedule: # Every Friday at 22:00 - rerun pipeline to check for dependency-based issues - cron: '0 22 * * 5' jobs: Prepare: uses: pyTooling/Actions/.github/workflows/PrepareJob.yml@r7 ConfigParams: uses: pyTooling/Actions/.github/workflows/ExtractConfiguration.yml@r7 UnitTestingParams: uses: pyTooling/Actions/.github/workflows/Parameters.yml@r7 with: package_namespace: 'pyTooling.*' version_file: 'Common/__init__.py' python_version: '3.13' python_version_list: '3.11 3.12 3.13 3.14 pypy-3.11' system_list: 'ubuntu ubuntu-arm windows windows-arm macos macos-arm mingw64 ucrt64' disable_list: 'ubuntu-arm:* windows-arm:pypy-3.11' PlatformTestingParams: uses: pyTooling/Actions/.github/workflows/Parameters.yml@r7 with: name: 'Platform' package_namespace: 'pyTooling.*' version_file: 'Common/__init__.py' python_version_list: '' system_list: 'ubuntu ubuntu-arm windows windows-arm macos macos-arm mingw32 mingw64 ucrt64 clang64' disable_list: 'ubuntu-arm:* mingw32:*' # no ruamel-yaml for MinGW32 pipeline-delay: 30 InstallParams: uses: pyTooling/Actions/.github/workflows/Parameters.yml@r7 needs: - UnitTestingParams with: package_namespace: 'pyTooling.*' version_file: 'Common/__init__.py' python_version: ${{ needs.UnitTestingParams.outputs.python_version }} python_version_list: '' system_list: 'ubuntu ubuntu-arm windows windows-arm macos macos-arm mingw64 ucrt64' UnitTesting: uses: pyTooling/Actions/.github/workflows/UnitTesting.yml@r7 needs: - ConfigParams - UnitTestingParams with: jobs: ${{ needs.UnitTestingParams.outputs.python_jobs }} pacboy: 'msys/git' unittest_report_xml: ${{ needs.ConfigParams.outputs.unittest_report_xml }} coverage_report_xml: ${{ needs.ConfigParams.outputs.coverage_report_xml }} coverage_report_json: ${{ needs.ConfigParams.outputs.coverage_report_json }} coverage_report_html: ${{ needs.ConfigParams.outputs.coverage_report_html }} unittest_xml_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_xml }} coverage_sqlite_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_sqlite }} PlatformTesting: uses: pyTooling/Actions/.github/workflows/UnitTesting.yml@r7 needs: - ConfigParams - PlatformTestingParams with: jobs: ${{ needs.PlatformTestingParams.outputs.python_jobs }} unittest_directory: 'unit/Platform' requirements: '-r tests/unit/requirements.txt' unittest_report_xml: ${{ needs.ConfigParams.outputs.unittest_report_xml }} coverage_report_xml: ${{ needs.ConfigParams.outputs.coverage_report_xml }} coverage_report_json: ${{ needs.ConfigParams.outputs.coverage_report_json }} coverage_report_html: ${{ needs.ConfigParams.outputs.coverage_report_html }} unittest_xml_artifact: ${{ fromJson(needs.PlatformTestingParams.outputs.artifact_names).unittesting_xml }} coverage_sqlite_artifact: ${{ fromJson(needs.PlatformTestingParams.outputs.artifact_names).codecoverage_sqlite }} StaticTypeCheck: uses: pyTooling/Actions/.github/workflows/StaticTypeCheck.yml@r7 needs: - ConfigParams - UnitTestingParams with: python_version: ${{ needs.UnitTestingParams.outputs.python_version }} html_report: ${{ needs.ConfigParams.outputs.typing_report_html }} html_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).statictyping_html }} DocCoverage: uses: pyTooling/Actions/.github/workflows/CheckDocumentation.yml@r7 needs: - ConfigParams - UnitTestingParams with: python_version: ${{ needs.UnitTestingParams.outputs.python_version }} directory: ${{ needs.UnitTestingParams.outputs.package_directory }} # fail_below: 70 Package: uses: pyTooling/Actions/.github/workflows/Package.yml@r7 needs: - UnitTestingParams - UnitTesting - PlatformTesting with: python_version: ${{ needs.UnitTestingParams.outputs.python_version }} artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).package_all }} Install: uses: pyTooling/Actions/.github/workflows/InstallPackage.yml@r7 needs: - UnitTestingParams - InstallParams - Package with: jobs: ${{ needs.InstallParams.outputs.python_jobs }} wheel: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).package_all }} package_name: '${{ needs.UnitTestingParams.outputs.package_fullname }}.Common' PublishCoverageResults: uses: pyTooling/Actions/.github/workflows/PublishCoverageResults.yml@r7 needs: - ConfigParams - UnitTestingParams - UnitTesting - PlatformTesting if: success() || failure() with: coverage_report_json: ${{ needs.ConfigParams.outputs.coverage_report_json }} coverage_report_html: ${{ needs.ConfigParams.outputs.coverage_report_html }} coverage_json_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_json }} coverage_html_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_html }} codecov: 'true' codacy: 'true' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODACY_TOKEN: ${{ secrets.CODACY_TOKEN }} PublishTestResults: uses: pyTooling/Actions/.github/workflows/PublishTestResults.yml@r7 needs: - ConfigParams - UnitTestingParams - UnitTesting - PlatformTesting if: success() || failure() with: testsuite-summary-name: ${{ needs.ConfigParams.outputs.package_fullname }} merged_junit_filename: ${{ fromJson(needs.ConfigParams.outputs.unittest_merged_report_xml).filename }} merged_junit_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_xml }} dorny: 'true' codecov: 'true' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # VerifyDocs: # uses: pyTooling/Actions/.github/workflows/VerifyDocs.yml@r7 # needs: # - UnitTestingParams # with: # python_version: ${{ needs.UnitTestingParams.outputs.python_version }} IntermediateCleanUp: uses: pyTooling/Actions/.github/workflows/IntermediateCleanUp.yml@r7 needs: - UnitTestingParams - PublishCoverageResults - PublishTestResults if: success() || failure() with: sqlite_coverage_artifacts_prefix: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_sqlite }}- xml_unittest_artifacts_prefix: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_xml }}- Documentation: uses: pyTooling/Actions/.github/workflows/SphinxDocumentation.yml@r7 needs: - ConfigParams - UnitTestingParams - PublishTestResults - PublishCoverageResults # - VerifyDocs if: (success() || failure()) && needs.PublishTestResults.result == 'success' && needs.PublishCoverageResults.result == 'success' with: python_version: ${{ needs.UnitTestingParams.outputs.python_version }} coverage_report_json: ${{ needs.ConfigParams.outputs.coverage_report_json }} unittest_xml_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_xml }} coverage_json_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_json }} html_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_html }} latex_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_latex }} # PDFDocumentation: # uses: pyTooling/Actions/.github/workflows/LaTeXDocumentation.yml@r7 # needs: # - UnitTestingParams # - Documentation # with: # document: sphinx_reports # latex_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_latex }} # pdf_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_pdf }} PublishToGitHubPages: uses: pyTooling/Actions/.github/workflows/PublishToGitHubPages.yml@r7 needs: - UnitTestingParams - Documentation # - PDFDocumentation - PublishCoverageResults - StaticTypeCheck with: doc: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_html }} coverage: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_html }} typing: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).statictyping_html }} TriggerTaggedRelease: uses: pyTooling/Actions/.github/workflows/TagReleaseCommit.yml@r7 needs: - Prepare - UnitTesting - PlatformTesting # - StaticTypeCheck - Package - Install - PublishToGitHubPages if: needs.Prepare.outputs.is_release_commit permissions: contents: write # required for create tag actions: write # required for trigger workflow with: version: ${{ needs.Prepare.outputs.version }} auto_tag: ${{ needs.Prepare.outputs.is_release_commit }} secrets: inherit ReleasePage: uses: pyTooling/Actions/.github/workflows/PublishReleaseNotes.yml@r7 if: needs.Prepare.outputs.is_release_tag == 'true' needs: - Prepare - UnitTesting - PlatformTesting # - StaticTypeCheck - Package - Install - PublishToGitHubPages with: tag: ${{ needs.Prepare.outputs.version }} secrets: inherit PublishOnPyPI: uses: pyTooling/Actions/.github/workflows/PublishOnPyPI.yml@r7 if: startsWith(github.ref, 'refs/tags') needs: - UnitTestingParams - ReleasePage with: python_version: ${{ needs.UnitTestingParams.outputs.python_version }} requirements: '-r dist/requirements.txt' artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).package_all }} secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} ArtifactCleanUp: uses: pyTooling/Actions/.github/workflows/ArtifactCleanUp.yml@r7 needs: - UnitTestingParams - PlatformTestingParams - StaticTypeCheck - PlatformTesting - Documentation # - PDFDocumentation - PublishTestResults - PublishCoverageResults - PublishToGitHubPages # - PublishOnPyPI - Install - IntermediateCleanUp with: package: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).package_all }} remaining: | ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_xml }}-* ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_html }}-* ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_sqlite }}-* ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_xml }}-* ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_json }}-* ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_html }}-* ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_xml }} ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_html }} ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_sqlite }} ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_xml }} ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_json }} ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_html }} ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).statictyping_html }} ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_html }} ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_latex }} ${{ fromJson(needs.PlatformTestingParams.outputs.artifact_names).unittesting_xml }}-* ${{ fromJson(needs.PlatformTestingParams.outputs.artifact_names).unittesting_html }}-* ${{ fromJson(needs.PlatformTestingParams.outputs.artifact_names).codecoverage_sqlite }}-* ${{ fromJson(needs.PlatformTestingParams.outputs.artifact_names).codecoverage_xml }}-* ${{ fromJson(needs.PlatformTestingParams.outputs.artifact_names).codecoverage_json }}-* ${{ fromJson(needs.PlatformTestingParams.outputs.artifact_names).codecoverage_html }}-* # ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_pdf }} pyTooling-8.11.0/.gitignore000066400000000000000000000006711513317154500155660ustar00rootroot00000000000000# Python cache and object files __pycache__/ *.py[cod] # Coverage.py .coverage .cov coverage.xml /report/coverage # mypy /report/typing # pytest /report/unit # bandit /report/bandit # setuptools /build/**/*.* /dist/**/*.* /*.egg-info # Dependencies !requirements.txt # Sphinx doc/_build/ doc/pyTooling/**/*.* !doc/pyTooling/index.rst # BuildTheDocs doc/_theme/**/*.* # PyCharm project files /.idea/workspace.xml # Git files !.git* pyTooling-8.11.0/.idea/000077500000000000000000000000001513317154500145525ustar00rootroot00000000000000pyTooling-8.11.0/.idea/inspectionProfiles/000077500000000000000000000000001513317154500204315ustar00rootroot00000000000000pyTooling-8.11.0/.idea/inspectionProfiles/profiles_settings.xml000066400000000000000000000002561513317154500247210ustar00rootroot00000000000000 pyTooling-8.11.0/.idea/modules.xml000066400000000000000000000004161513317154500167450ustar00rootroot00000000000000 pyTooling-8.11.0/.idea/pyTooling.Common.iml000066400000000000000000000015411513317154500204710ustar00rootroot00000000000000 pyTooling-8.11.0/.vscode/000077500000000000000000000000001513317154500151335ustar00rootroot00000000000000pyTooling-8.11.0/.vscode/settings.json000066400000000000000000000000531513317154500176640ustar00rootroot00000000000000{ "files.trimTrailingWhitespace": false, } pyTooling-8.11.0/LICENSE.md000066400000000000000000000245071513317154500152060ustar00rootroot00000000000000This is a local copy of the Apache License Version 2.0. The original can be obtained here: [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) -------------------------------------------------------------------------------- # Apache License Version 2.0, January 2004 ## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION ### 1. Definitions. *"License"* shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. *"Licensor"* shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. *"Legal Entity"* shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. *"You"* (or *"Your"*) shall mean an individual or Legal Entity exercising permissions granted by this License. *"Source"* form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. *"Object"* form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. *"Work"* shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). *"Derivative Works"* shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. *"Contribution"* shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, *"submitted"* means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as *"Not a Contribution."* *"Contributor"* shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. ### 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. ### 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. ### 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - You must give any other recipients of the Work or Derivative Works a copy of this License; and - You must cause any modified files to carry prominent notices stating that You changed the files; and - You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. ### 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. ### 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. ### 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. ### 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. ### 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ## Appendix: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pyTooling-8.11.0/README.md000066400000000000000000000425051513317154500150570ustar00rootroot00000000000000[![Sourcecode on GitHub](https://img.shields.io/badge/pyTooling-pyTooling-63bf7f?longCache=true&style=flat-square&longCache=true&logo=GitHub)](https://GitHub.com/pyTooling/pyTooling) [![Sourcecode License](https://img.shields.io/pypi/l/pyTooling?longCache=true&style=flat-square&logo=Apache&label=code)](LICENSE.md) [![Documentation](https://img.shields.io/website?longCache=true&style=flat-square&label=pyTooling.github.io%2FpyTooling&logo=GitHub&logoColor=fff&up_color=blueviolet&up_message=Read%20now%20%E2%9E%9A&url=https%3A%2F%2FpyTooling.github.io%2FpyTooling%2Findex.html)](https://pyTooling.github.io/pyTooling/) [![Documentation License](https://img.shields.io/badge/doc-CC--BY%204.0-green?longCache=true&style=flat-square&logo=CreativeCommons&logoColor=fff)](LICENSE.md) [![PyPI](https://img.shields.io/pypi/v/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072)](https://pypi.org/project/pyTooling/) ![PyPI - Status](https://img.shields.io/pypi/status/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072) [![GitHub Workflow - Build and Test Status](https://img.shields.io/github/actions/workflow/status/pyTooling/pyTooling/Pipeline.yml?branch=main&longCache=true&style=flat-square&label=build%20and%20test&logo=GitHub%20Actions&logoColor=FFFFFF)](https://GitHub.com/pyTooling/pyTooling/actions/workflows/Pipeline.yml) [![Libraries.io status for latest release](https://img.shields.io/librariesio/release/pypi/pyTooling?longCache=true&style=flat-square&logo=Libraries.io&logoColor=fff)](https://libraries.io/github/pyTooling/pyTooling) [![Codacy - Quality](https://img.shields.io/codacy/grade/08ef744c0b70490289712b02a7a4cebe?longCache=true&style=flat-square&logo=Codacy)](https://www.codacy.com/gh/pyTooling/pyTooling) [![Codacy - Coverage](https://img.shields.io/codacy/coverage/08ef744c0b70490289712b02a7a4cebe?longCache=true&style=flat-square&logo=Codacy)](https://www.codacy.com/gh/pyTooling/pyTooling) [![Codecov - Branch Coverage](https://img.shields.io/codecov/c/github/pyTooling/pyTooling?longCache=true&style=flat-square&logo=Codecov)](https://codecov.io/gh/pyTooling/pyTooling) # pyTooling **pyTooling** is a powerful collection of arbitrary useful abstract data models, missing classes, decorators, a new performance boosting meta-class and enhanced exceptions. It also provides lots of helper functions e.g. to ease the handling of package descriptions or to unify multiple existing APIs into a single API. It's useful - if not even essential - for **any** Python-base project independent if it's a library, framework, CLI tool or just a "script". In addition, pyTooling provides a collection of [CI job templates for GitHub Actions](https://github.com/pyTooling/Actions). This drastically simplifies GHA-based CI pipelines for Python projects. ## Package Details ### Attributes The [pyTooling.Attributes] module offers the base implementation of *.NET-like attributes* realized with Python decorators. The annotated and declarative data is stored as instances of Attribute classes in an additional field per class, method or function. The annotation syntax (decorator syntax) allows users to attach any structured data to classes, methods or functions. In many cases, a user will derive a custom attribute from Attribute and override the __init__ method, so user-defined parameters can be accepted when the attribute is constructed. Later, classes, methods or functions can be searched for by querying the attribute class for attribute instance usage locations (see example to the right). Another option for class and method attributes is declaring a classes using pyTooling’s `ExtendedType` meta-class. Here the class itself offers helper methods for discovering annotated methods. A `SimpleAttribute` class is offered accepting any positional and keyword parameters. In a more advanced use case, users are encouraged to derive their own attribute class hierarchy from `Attribute`. #### Use Cases In general all classes, methods and functions can be annotated with additional meta-data. It depends on the application, framework or library to decide if annotations should be applied imperatively as regular code or declaratively as attributes via Python decorators. **With this in mind, the following use-cases and ideas can be derived:** * Describe a command line argument parser (like ArgParse) in a declarative form. |br| See [pyTooling.Attributes.ArgParse Package and Examples](https://pytooling.github.io/pyTooling/Attributes/ArgParse.html) * Mark nested classes, so later when the outer class gets instantiated, these nested classes are indexed or automatically registered. See [CLIAbstraction](https://pytooling.github.io/pyTooling/CLIAbstraction/index.html) → [CLIABS/CLIArgument] * Mark methods in a class as test cases and classes as test suites, so test cases and suites are not identified based on a magic method name. *Investigation ongoing / planned feature.* #### Using `SimpleAttribute` ````Python from pyTooling.Attributes import SimpleAttribute @SimpleAttribute(kind="testsuite") class MyClass: @SimpleAttribute(kind="testcase", id=1, description="Test and operator") def test_and(self): ... @SimpleAttribute(kind="testcase", id=2, description="Test xor operator") def test_xor(self): ... ```` ### CLI Abstraction [pyTooling.CLIAbstraction] offers an abstraction layer for command line programs, so they can be used easily in Python. There is no need for manually assembling parameter lists or considering the order of parameters. All parameters like `-v` or `--value=42` are described as [CommandLineArgument] instances on a [Program] class. Each argument class like [ShortFlag] or [PathArgument] knows about the correct formatting pattern, character escaping, and if needed about necessary type conversions. A program instance can be converted to an argument list suitable for [subprocess.Popen]. While a user-defined command line program abstraction derived from [Program] only takes care of maintaining and assembling parameter lists, a more advanced base-class, called [Executable], is offered with embedded [subprocess.Popen] behavior. #### Design Goals * Offer access to CLI programs as Python classes. * Abstract CLI arguments (a.k.a. parameter, option, flag, ...) as members on such a Python class. * Abstract differences in operating systems like argument pattern (POSIX: `-h` vs. Windows: `/h`), path delimiter signs (POSIX: `/` vs. Windows: `\`) or executable names. * Derive program variants from existing programs. * Assemble parameters as list for handover to [subprocess.Popen] with proper escaping and quoting. * Launch a program with :class:[subprocess.Popen] and hide the complexity of Popen. * Get a generator object for line-by-line output reading to enable postprocessing of outputs. ### Common Helper Functions This is a set of useful [helper functions](https://pytooling.github.io/pyTooling/Common/index.html#common-helperfunctions): * [getsizeof](https://pytooling.github.io/pyTooling/Common/index.html#getsizeof) calculates the "real" size of a data structure. * [isnestedclass](https://pytooling.github.io/pyTooling/Common/index.html#isnestedclass) checks if a class is nested inside another class. * [firstKey](https://pytooling.github.io/pyTooling/Common/index.html#firstkey), [firstValue](https://pytooling.github.io/pyTooling/Common/index.html#firstvalue), [firstPair](https://pytooling.github.io/pyTooling/Common/index.html#firstitem) get the firstItem key/value/item from an ordered dictionary. * [mergedicts](https://pytooling.github.io/pyTooling/Common/index.html#mergedicts) merges multiple dictionaries into a new dictionary. * [zipdicts](https://pytooling.github.io/pyTooling/Common/index.html#zipdicts) iterate multiple dictionaries simultaneously. ### Common Classes * [Call-by-reference parameters](https://pytooling.github.io/pyTooling/Common/CallByRef.html): Python doesn't provide *call-by-reference parameters* for simple types. This behavior can be emulated with classes provided by the `pyTooling.CallByRef` module. * [Unified license names](https://pytooling.github.io/pyTooling/Common/Licensing.html): Setuptools, PyPI, and others have a varying understanding of license names. The `pyTooling.Licensing` module provides *unified license names* as well as license name mappings or translations. * [Unified platform and environment description](https://pytooling.github.io/pyTooling/Common/Platform.html): Python has many ways in figuring out the current platform using APIs from `sys`, `platform`, `os`, …. Unfortunately, none of the provided standard APIs offers a comprehensive answer. pyTooling provides a `CurrentPlatform` singleton summarizing multiple platform APIs into a single class instance. * [Representations of version numbers](https://pytooling.github.io/pyTooling/Common/Versioning.html): While Python itself has a good versioning schema, there are no classes provided to abstract version numbers. pyTooling provides such representations following semantic versioning (SemVer) and calendar versioning (CalVer) schemes. It's provided by the `pyTooling.Versioning` module. ### Configuration Various file formats suitable for configuration information share the same features supporting: key-value pairs (dictionaries), sequences (lists), and simple types like string, integer and float. pyTooling provides an [abstract configuration file data model](https://pytooling.github.io/pyTooling/Configuration/index.html) supporting these features. Moreover, concrete [configuration file format reader](https://pytooling.github.io/pyTooling/Configuration/FileFormats.html) implementations are provided as well. * [JSON configuration reader](https://pytooling.github.io/pyTooling/Configuration/JSON.html) for the JSON file format. * [TOML configuration reader](https://pytooling.github.io/pyTooling/Configuration/TOML.html) → To be implemented. * [YAML configuration reader](https://pytooling.github.io/pyTooling/Configuration/YAML.html) for the YAML file format. ### Data Structures pyTooling also provides [fast and powerful data structures](https://pytooling.github.io/pyTooling/DataStructures/index.html) offering object-oriented APIs: * [Graph data structure](https://pytooling.github.io/pyTooling/DataStructures/Graph.html) → A directed graph implementation using a `Vertex` and an `Edge` class. * [Path data structure](https://pytooling.github.io/pyTooling/DataStructures/Path/index.html) → To be documented. * [Finite State Machine data structure](https://pytooling.github.io/pyTooling/DataStructures/StateMachine.html) → A data model for state machines using a `State` and a `Transition` class. * [Tree data structure](https://pytooling.github.io/pyTooling/DataStructures/Tree.html) → A fast and simple implementation using a single `Node` class. ### Decorators * [Abstract Methods](https://pytooling.github.io/pyTooling/MetaClasses.html#meta-abstract) * Methods marked with `abstractmethod` are abstract and need to be overwritten in a derived class. An *abstract method* might be called from the overwriting method. * Methods marked with `mustoverride` are abstract and need to be overridden in a derived class. It's not allowed to call a *mustoverride method*. * [Documentation](https://pytooling.github.io/pyTooling/Decorators.html#deco-documentation) * Copy the doc-string from given base-class via `InheritDocString`. * [Visibility](https://pytooling.github.io/pyTooling/Decorators.html#deco-visibility) * Register the given function or class as publicly accessible in a module via `export`. * [Documentation](https://pyTooling.GitHub.io/pyTooling/Decorators.html#documentation) * [`@InheritDocString`](https://pyTooling.GitHub.io/pyTooling/Decorators.html#inheritdocstring) → Copy the doc-string from given base-class. * [Visibility](https://pyTooling.GitHub.io/pyTooling/Decorators.html#visibility) * [`@export`](https://pyTooling.GitHub.io/pyTooling/Decorators.html#export) → Register the given function or class as publicly accessible in a module. ### Exceptions * [EnvironmentException](https://pyTooling.GitHub.io/pyTooling/Exceptions.html#environmentexception) ... is raised when an expected environment variable is missing. * [PlatformNotSupportedException](https://pyTooling.GitHub.io/pyTooling/Exceptions.html#platformnotsupportedexception) ... is raise if the platform is not supported. * [NotConfiguredException](https://pyTooling.GitHub.io/pyTooling/Exceptions.html#notconfiguredexception) ... is raise if the requested setting is not configured. ### Meta-Classes pyTooling provides an [enhanced meta-class](https://pytooling.github.io/pyTooling/MetaClasses.html) called `ExtendedType`. This meta-classes allows to implement [abstract methods](https://pytooling.github.io/pyTooling/MetaClasses.html#abstract-method), [singletons](https://pytooling.github.io/pyTooling/MetaClasses.html#singleton), [slotted types](https://pytooling.github.io/pyTooling/MetaClasses.html#slotted-type) and combinations thereof. `class MyClass(metaclass=ExtendedType):` A class definition using that meta-class can implement [abstract methods](https://pytooling.github.io/pyTooling/MetaClasses.html#abstract-method) using decorators `@abstractmethod` or `@mustoverride`. `class MyClass(metaclass=ExtendedType, singleton=True):` A class defined with enabled [singleton](https://pytooling.github.io/pyTooling/MetaClasses.html#singleton) behavior allows only a single instance of that class to exist. If another instance is going to be created, a previously cached instance of that class will be returned. `class MyClass(metaclass=ExtendedType, slots=True):` A class defined with enabled [slots](https://pytooling.github.io/pyTooling/MetaClasses.html#slotted-type) behavior stores instance fields in slots. The meta-class, translates all type-annotated fields in a class definition into slots. Slots allow a more efficient field storage and access compared to dynamically stored and accessed fields hosted by `__dict__`. This improves the memory footprint as well as the field access performance of all class instances. This behavior is automatically inherited to all derived classes. `class MyClass(ObjectWithSlots):` A class definition deriving from `ObjectWithSlots` will bring the slotted type behavior to that class and all derived classes. ### Packaging A set of helper functions to describe a Python package for setuptools. * Helper Functions: * `loadReadmeFile` Load a `README.md` file from disk and provide the content as long description for setuptools. * `loadRequirementsFile` Load a `requirements.txt` file from disk and provide the content for setuptools. * `extractVersionInformation` Extract version information from Python source files and provide the data to setuptools. * Package Descriptions * `DescribePythonPackage` tbd * `DescribePythonPackageHostedOnGitHub` tbd ### Terminal A set of helpers to implement a text user interface (TUI) in a terminal. #### Features * Colored command line outputs based on `colorama`. * Message classification in `fatal`, `error`, `warning`, `normal`, `quiet`, ... * Get information like terminal dimensions from underlying terminal window. #### Simple Terminal Application This is a minimal terminal application example which inherits from `LineTerminal`. ```python from pyTooling.TerminalUI import TerminalApplication class Application(TerminalApplication): def __init__(self) -> None: super().__init__() def run(self): self.WriteNormal("This is a simple application.") self.WriteWarning("This is a warning message.") self.WriteError("This is an error message.") # entry point if __name__ == "__main__": Application.CheckPythonVersion((3, 6, 0)) app = Application() app.run() app.Exit() ``` ### Stopwatch *tbd* ## Examples ### `@export` Decorator ```Python from pyTooling.Decorators import export @export class MyClass: pass ``` ### `CallByRefIntParam` ```Python from pyTooling.CallByRef import CallByRefIntParam # define a call-by-reference parameter for integer values myInt = CallByRefIntParam(3) # a function using a call-by-reference parameter def func(param: CallByRefIntParam): param <<= param * 4 # call the function and pass the wrapper object func(myInt) print(myInt.Value) ``` ## Contributors * [Patrick Lehmann](https://GitHub.com/Paebbels) (Maintainer) * [Sven Köhler](https://GitHub.com/skoehler) * [Unai Martinez-Corral](https://github.com/umarcor) * [and more...](https://GitHub.com/pyTooling/pyTooling/graphs/contributors) ## License This Python package (source code) licensed under [Apache License 2.0](LICENSE.md). The accompanying documentation is licensed under [Creative Commons - Attribution 4.0 (CC-BY 4.0)](doc/Doc-License.rst). ------------------------- SPDX-License-Identifier: Apache-2.0 pyTooling-8.11.0/dist/000077500000000000000000000000001513317154500145355ustar00rootroot00000000000000pyTooling-8.11.0/dist/requirements.txt000066400000000000000000000000351513317154500200170ustar00rootroot00000000000000wheel ~= 0.45.0 twine ~= 6.2 pyTooling-8.11.0/doc/000077500000000000000000000000001513317154500143375ustar00rootroot00000000000000pyTooling-8.11.0/doc/Attributes/000077500000000000000000000000001513317154500164655ustar00rootroot00000000000000pyTooling-8.11.0/doc/Attributes/ArgParse.rst000066400000000000000000000107001513317154500207210ustar00rootroot00000000000000.. _ATTR/ArgParse: ArgParse ######## Many people use Python's :mod:`argparse` command line argument parser. This parser can handle sub-commands like ``git commit -m "message"`` where *commit* is a sub-command and ``-m `` is an argument of this sub-command parser. It's possible to assign a callback function to each individual sub-command parser. .. rubric:: Advantages * Declarative description instead of imperative form. * All options from argparse can be used. * Declare accepted command-line arguments close to the responsible handler method * Complex parsers can be distributed accross multiple classes and merged via multiple inheritance. * Pre-defined argument templates like switch parameters (``--help``). .. _ATTR/ArgParse/Comparison: Comparison ********** .. grid:: 2 .. grid-item:: **pyTooling.Attributes.ArgParse** .. code-block:: Python class Program: @DefaultHandler() @FlagArgument(short="-v", long="--verbose", dest="verbose", help="Show verbose messages.") def HandleDefault(self, args) -> None: pass @CommandHandler("new-user", help="Add a new user.") @StringArgument(dest="username", metaName="username", help="Name of the new user.") @LongValuedFlag("--quota", dest="quota", help="Max usable disk space.") def NewUserHandler(self, args) -> None: pass @CommandHandler("delete-user", help="Delete a user.") @StringArgument(dest="username", metaName="username", help="Name of the user.") @FlagArgument(short="-f", long="--force", dest="force", help="Ignore internal checks.") def DeleteUserHandler(self, args) -> None: pass @CommandHandler("list-user", help="List all users.") def ListUserHandler(self, args) -> None: pass .. grid-item:: **Traditional ArgParse** .. code-block:: Python class Program: def __init__(self): mainParser = argparse.ArgumentParser() mainParser.set_defaults(func=self.HandleDefault) mainParser.add_argument("-v", "--verbose") subParsers = mainParser.add_subparsers() newUserParser = subParsers.add_parser("new-user", help="Add a new user.") newUserParser.add_argument(dest="username", metaName="username", help="Name of the new user.") newUserParser.add_argument("--quota", dest="quota", help="Max usable disk space.") newUserParser.set_defaults(func=self.NewUserHandler) deleteUserParser = subParsers.add_parser("delete-user", help="Delete a user.") deleteUserParser.add_argument(dest="username", metaName="username", help="Name of the user.") deleteUserParser.add_argument("-f", "--force", dest="force", help="Ignore internal checks.") deleteUserParser.set_defaults(func=self.DeleteUserHandler) listUserParser = subParsers.add_parser("list-user", help="List all users.") listUserParser.set_defaults(func=self.ListUserHandler) def HandleDefault(self, args) -> None: pass def NewUserHandler(self, args) -> None: pass def DeleteUserHandler(self, args) -> None: pass def ListUserHandler(self, args) -> None: pass .. _ATTR/ArgParse/Arguments: Arguments ********* .. _ATTR/ArgParse/Flags: Flags ===== .. _ATTR/ArgParse/ValuedFlags: ValuedFlags =========== .. _ATTR/ArgParse/ValuedTupleFlags: ValuedTupleFlags ================ .. _ATTR/ArgParse/Lists: Argument Lists ************** .. _ATTR/ArgParse/Commands: Commands ******** .. _ATTR/ArgParse/Grouping: Grouping Arguments ****************** .. _ATTR/ArgParse/MixIn: Split Handlers into multiple classes ************************************ Classic ``argparse`` Example **************************** .. literalinclude:: ../../tests/example/OldStyle.py :language: python :linenos: :caption: tests/example/OldStyle.py :tab-width: 2 New ``pyTooling.Attributes`` Approach ************************************* A better and more descriptive solution could look like this: .. literalinclude:: ../../tests/example/UserManager.py :language: python :linenos: :caption: tests/example/UserManager.py :tab-width: 2 .. _ATTR/ArgParse/Consumers: Consumers ********* This package is used by: * ✅ . |br| :ref:`pyTooling.Attributes.ArgParse ` pyTooling-8.11.0/doc/Attributes/index.rst000066400000000000000000000326261513317154500203370ustar00rootroot00000000000000.. _ATTR: Overview ######## The :mod:`pyTooling.Attributes` package offers the base implementation of `.NET-like attributes `__ realized with :term:`Python decorators `. The annotated and declarative data is stored as instances of :class:`~pyTooling.Attributes.Attribute` classes in an additional ``__pyattr__`` field per class, method or function. The annotation syntax allows users to attach any structured data to classes, methods or functions. In many cases, a user will derive a custom attribute from :class:`~pyTooling.Attributes.Attribute` and override the ``__init__`` method, so user-defined parameters can be accepted when the attribute is constructed. Later, classes, methods or functions can be searched for by querying the attribute class for attribute instance usage locations (see 'Function Attributes' example). Another option for class and method attributes is defining a new classes using pyTooling’s :ref:`META/ExtendedType` meta-class. Here the class itself offers helper methods for discovering annotated methods (see 'Method Attributes' example). While all *user-defined* (and *pre-defined*) attributes offer a powerful API derived from :class:`~pyTooling.Attributes.Attribute` class, the full potential can only be experienced when using class declarations constructed by the :class:`pyTooling.MetaClass.ExtendedType` meta-class. Attributes can create a complex class hierarchy. This helps in finding and filtering for annotated data. .. _ATTR/Goals: Design Goals ************ The main design goals are: * Allow meta-data annotation to Python language entities (class, method, function) as declarative syntax. * Find applied attributes based on attribute type (methods on the attribute). * Find applied attributes in a scope (find on class and on class' methods). * Allow building a hierarchy of attribute classes. * Filter attributes based on their class hierarchy. * Reduce overhead to class creation time (do not impact object creation time). .. _ATTR/Example: Example ******* .. grid:: 2 .. grid-item:: **Function Attributes** :columns: 6 .. code-block:: Python from pyTooling.Attributes import Attribute class Command(Attribute): def __init__(self, cmd: str, help: str = ""): pass class Flag(Attribute): def __init__(self, param: str, short: str = None, long: str = None, help: str = ""): pass @Command(cmd="version", help="Print version information.") @Flag(param="verbose", short="-v", long="--verbose", help="Default handler.") def Handler(self, args): pass for function in Command.GetFunctions(): pass .. grid-item:: **Method Attributes** :columns: 6 .. code-block:: Python from pyTooling.Attributes import Attribute from pyTooling.MetaClasses import ExtendedType class TestCase(Attribute): def __init__(self, name: str): pass class Program(metaclass=ExtendedType): @TestCase(name="Handler routine") def Handler(self, args): pass prog = Program() for method, attributes in prog.GetMethodsWithAttributes(predicate=TestCase): pass .. _ATTR/UseCases: Use Cases ********* In general all classes, methods and functions can be annotated with additional meta-data. It depends on the application, framework or library to decide if annotations should be applied imperatively as regular code or declaratively as attributes via Python decorators. With this in mind, the following use-cases and ideas can be derived: .. rubric:: Derived Use Cases: * Describe a command line argument parser (like ArgParse) in a declarative form. |br| See :ref:`pyTooling.Attributes.ArgParse Package and Examples ` * Mark nested classes, so later when the outer class gets instantiated, these nested classes are indexed or automatically registered. |br| See :ref:`CLIAbstraction ` |rarr| :ref:`CLIABS/CLIArgument` * Mark methods in a class as test cases and classes as test suites, so test cases and suites are not identified based on a magic method name. |br| *Investigation ongoing / planned feature.* * Mark class members as public or private and control visibility in auto-generated documentation. |br| See `SphinxExtensions `_ |rarr| DocumentMemberAttribute .. _ATTR/Predefined: Predefined Attributes ********************* pyTooling's attributes offers the :class:`~pyTooling.Attributes.Attribute` base-class to derive further attribute classes. A derive :class:`~pyTooling.Attributes.SimpleAttribute` is also offered to accept any ``*args, **kwargs`` parameters for annotation of semi-structured meta-data. It's recommended to derive an own hierarchy of attribute classes with well-defined parameter lists for the ``__init__`` method. Meta-data stored in attribute should be made accessible via (readonly) properties. In addition, an :mod:`pyTooling.Attributes.ArgParse` subpackage is provided, which allows users to describe complex argparse command line argument parser structures in a declarative way. .. rubric:: Partial inheritance diagram: .. inheritance-diagram:: pyTooling.Attributes.SimpleAttribute pyTooling.Attributes.ArgParse.DefaultHandler pyTooling.Attributes.ArgParse.CommandHandler pyTooling.Attributes.ArgParse.CommandLineArgument :parts: 1 .. _ATTR/Predefined/Attribute: Attribute ========= The :class:`~pyTooling.Attributes.Attribute` class implements half of the attribute's feature set. It implements the instantiation and storage of attribute internal values as well as the search and lookup methods to find attributes. The second half is implemented in the :class:`~pyTooling.MetaClasses.ExtendedType` meta-class. It adds attribute specific methods to each class created by that meta-class. Any attribute is applied on a class, method or function using Python's decorator syntax, because every attribute is actually a decorator. In addition, such a decorator accepts parameters, which are used to instantiate an attribute class and handover the parameters to that attribute instance. Every instance of an attribute is registered at its class in a class variable. Further more, these instances are distinguished if they are applied to a class, method or function. * :meth:`~pyTooling.Attributes.Attribute.GetClasses` returns a generator to iterate all classes, this attribute was applied to. * :meth:`~pyTooling.Attributes.Attribute.GetMethods` returns a generator to iterate all methods, this attribute was applied to. * :meth:`~pyTooling.Attributes.Attribute.GetFunctions` returns a generator to iterate all functions, this attribute was applied to. * :meth:`~pyTooling.Attributes.Attribute.GetAttributes` returns a tuple of applied attributes to the given method. .. grid:: 3 .. grid-item:: **Apply a class attribute** :columns: 4 .. code-block:: Python from pyTooling.Attributes import Attribute @Attribute() class MyClass: pass .. grid-item:: **Apply a method attribute** :columns: 4 .. code-block:: Python from pyTooling.Attributes import Attribute class MyClass: @Attribute() def MyMethod(self): pass .. grid-item:: **Apply a function attribute** :columns: 4 .. code-block:: Python from pyTooling.Attributes import Attribute @Attribute() def MyFunction(param): pass .. grid:: 3 .. grid-item:: **Find attribute usages of class attributes** :columns: 4 .. code-block:: Python from pyTooling.Attributes import Attribute for cls in Attribute.GetClasses(): pass .. grid-item:: **Find attribute usages of method attributes** :columns: 4 .. code-block:: Python from pyTooling.Attributes import Attribute for method in Attribute.GetMethods(): pass .. grid-item:: **Find attribute usages of function attributes** :columns: 4 .. code-block:: Python from pyTooling.Attributes import Attribute for function in Attribute.GetFunctions(): pass .. rubric:: Condensed definition of class :class:`~pyTooling.Attributes.Attribute` .. code-block:: Python class Attribute: @classmethod def GetFunctions(cls, scope: Type = None, predicate: TAttributeFilter = None) -> Generator[TAttr, None, None]: ... @classmethod def GetClasses(cls, scope: Type = None, predicate: TAttributeFilter = None) -> Generator[TAttr, None, None]: ... @classmethod def GetMethods(cls, scope: Type = None, predicate: TAttributeFilter = None) -> Generator[TAttr, None, None]: ... @classmethod def GetAttributes(cls, method: MethodType, includeSubClasses: bool = True) -> Tuple['Attribute', ...]: ... .. rubric:: Planned Features * Allow attributes to be applied only once per kind. * Allow limitation of attributes to classes, methods or functions, so an attribute meant for methods can't be applied to a function or class. * Allow filtering attribute with a predicate function, so values of an attribute instance can be checked too. .. _ATTR/Predefined/SimpleAttribute: SimpleAttribute =============== The :class:`~pyTooling.Attributes.SimpleAttribute` class accepts any positional and any keyword arguments as data. That data is made available via :attr:`~pyTooling.Attributes.SimpleAttribute.Args` and :attr:`~pyTooling.Attributes.SimpleAttribute.KwArgs` properties. .. code-block:: Python from pyTooling.Attributes import SimpleAttribute @SimpleAttribute(kind="testsuite") class MyClass: @SimpleAttribute(kind="testcase", id=1, description="Test and operator") def test_and(self): ... @SimpleAttribute(kind="testcase", id=2, description="Test xor operator") def test_xor(self): ... **Condensed definition of class** :class:`~pyTooling.Attributes.SimpleAttribute`: .. code-block:: python class SimpleAttribute(Attribute): def __init__(self, *args, **kwargs) -> None: ... @readonly def Args(self) -> Tuple[Any, ...]: ... @readonly def KwArgs(self) -> Dict[str, Any]: ... .. _ATTR/UserDefined: User-Defined Attributes *********************** It's recommended to derive user-defined attributes from :class:`~pyTooling.Attributes.Attribute`, so the ``__init__`` method can be overriden to accept a well defined parameter list including type hints. The example defines an ``Annotation`` attribute, which accepts a single string parameter. When the attribute is applied, the parameter is stored in an instance. The inner field is then accessible via readonly ``Annotation`` property. .. grid:: 2 .. grid-item:: **Find attribute usages of class attributes** :columns: 6 .. code-block:: Python class Application(metaclass=ExtendedType): @Annotation("Some annotation data") def AnnotatedMethod(self): pass for method in Annotation.GetMethods(): pass .. grid-item:: **Find attribute usages of class attributes** :columns: 6 .. code-block:: python from pyTooling.Attributes import Attribute class Annotation(Attribute): _annotation: str def __init__(self, annotation: str): self._annotation = annotation @readonly def Annotation(self) -> str: return self._annotation .. _ATTR/Searching: Searching Attributes ******************** .. todo:: Attributes:: Searching Attributes .. _ATTR/Filtering: Filtering Attributes ******************** Methods :meth:`~pyTooling.Attributes.Attribute.GetClasses`, :meth:`~pyTooling.Attributes.Attribute.GetMethods` :meth:`~pyTooling.Attributes.Attribute.GetFunctions`, :meth:`~pyTooling.Attributes.Attribute.GetAttributes` accept an optional ``predicate`` parameter, which needs to be a subclass of :class:`~pyTooling.Attributes.Attribute`. .. todo:: Attributes:: Filtering Attributes .. _ATTR/Grouping: Grouping Attributes ******************* .. todo:: Attributes:: Grouping Attributes .. code-block:: Python from pyTooling.Attributes import Attribute, SimpleAttribute class GroupAttribute(Attribute): _id: str def __init__(self, id: str): self._id = id def __call__(self, entity: Entity) -> Entity: self._AppendAttribute(entity, SimpleAttribute(3, 4, id=self._id, name="attr1")) self._AppendAttribute(entity, SimpleAttribute(5, 6, id=self._id, name="attr2")) return entity class Grouped(TestCase): def test_Group_Simple(self) -> None: @SimpleAttribute(1, 2, id="my", name="Class1") @GroupAttribute("grp") class MyClass1: pass .. _ATTR/Details: Implementation Details ********************** .. todo:: Attributes:: Implementation details :data:`~pyTooling.Attributes.ATTRIBUTES_MEMBER_NAME` The annotated data is stored in an additional ``__dict__`` entry for each annotated method. By default the entry is called ``__pyattr__``. Multiple attributes can be applied to the same method. .. _ATTR/Consumers: Consumers ********* This abstraction layer is used by: * ✅ Declarative definition of ArgParse parser rules. |br| :ref:`pyTooling.Attributes.ArgParse ` pyTooling-8.11.0/doc/CLIAbstraction/000077500000000000000000000000001513317154500171405ustar00rootroot00000000000000pyTooling-8.11.0/doc/CLIAbstraction/Arguments.rst000066400000000000000000000341771513317154500216530ustar00rootroot00000000000000.. _CLIABS/Arguments: Arguments ######### .. todo:: Naming convention * Basic classes |rarr| Argument * Named arguments |rarr| Flag * Character prefixes |rarr| Short, Long, Windows .. _CLIABS/Arguments:Overview: Overview ******** .. mermaid:: graph LR; CLA[CommandLineArgument] style CLA stroke-dasharray: 5 5 EA[ExecutableArgument] NA[NamedArgument] style NA stroke-dasharray: 5 5 VA[ValuedArgument] style VA stroke-dasharray: 5 5 NVA[NamedAndValuedArgument] style NVA stroke-dasharray: 5 5 BF[BooleanFlag] style NVA stroke-dasharray: 5 5 NTA[NamedTupledArgument] style NTA stroke-dasharray: 5 5 NKVPA[NamedKeyValuePairsArgument] style NKVPA stroke-dasharray: 5 5 CLA ----> EA CLA --> NA CLA --> VA NA --> NVA VA --> NVA NA --> BF VA --> BF NA --> NTA VA --> NTA NA --> NKVPA VA --> NKVPA CA["CommandArgument
command
"] FA[FlagArgument] style FA stroke-dasharray: 5 5 NA ---> CA NA ---> FA SA["StringArgument
value
"] SLA["StringListArgument
value1 value2
"] PA["PathArgument
file1.txt
"] PLA["PathListArgument
file1.txt file2.txt
"] VA ---> SA VA ---> SLA VA ---> PA VA ---> PLA NVFA["NamedAndValuedFlagArgument
output=file.txt
"] style NVFA stroke-dasharray: 5 5 NOVFA["NamedAndOptionalValuedFlagArgument
output=file.txt
"] style NOVFA stroke-dasharray: 5 5 NVA --> NVFA NVA --> NOVFA .. _CLIABS/Arguments/WithPrefix: Without Prefix Character(s) *************************** +--------------------------+--------------------------------+-------------------------------------------------------------------+ | **RAW Format** | **Examples** | **Argument Class** | +--------------------------+--------------------------------+-------------------------------------------------------------------+ | ``executable`` | ``prog`` | :class:`~pyTooling.CLIAbstraction.Argument.ExecutableArgument` | +--------------------------+--------------------------------+-------------------------------------------------------------------+ | ``--`` | ``prog -option -- file1.txt`` | :class:`~pyTooling.CLIAbstraction.Argument.DelimiterArgument` | +--------------------------+--------------------------------+-------------------------------------------------------------------+ | ``command`` | ``prog help`` | :class:`~pyTooling.CLIAbstraction.Command.CommandArgument` | +--------------------------+--------------------------------+-------------------------------------------------------------------+ | ``string`` | ``prog value`` | :class:`~pyTooling.CLIAbstraction.Argument.StringArgument` | +--------------------------+--------------------------------+-------------------------------------------------------------------+ | ``string1`` ``string2`` | ``prog value1 value2`` | :class:`~pyTooling.CLIAbstraction.Argument.StringListArgument` | +--------------------------+--------------------------------+-------------------------------------------------------------------+ | ``path`` | ``prog file1.txt`` | :class:`~pyTooling.CLIAbstraction.Argument.PathArgument` | +--------------------------+--------------------------------+-------------------------------------------------------------------+ | ``path1`` ``path2`` | ``prog File1.log File1.log`` | :class:`~pyTooling.CLIAbstraction.Argument.PathListArgument` | +--------------------------+--------------------------------+-------------------------------------------------------------------+ Executable ========== An executable argument represents a program/executable. The internal value is a :class:`Path` object. Command ======= Commands are (usually) mutually exclusive arguments and the first argument in a list of arguments to a program. They are used to logically group arguments. While commands can or cannot have prefix characters, they shouldn't be confused with flag arguments or string arguments. **Example:** * ``prog command -arg1 --argument2`` .. seealso:: * For simple flags (various formats). |br| |rarr| :mod:`~pyTooling.CLIAbstraction.Flag` * For string arguments. |br| |rarr| :class:`~pyTooling.CLIAbstraction.Argument.StringArgument` String ====== A simple argument accepting any string value. If a string has a predefined format, more specific argument classes should be used like :mod:`~pyTooling.CLIAbstraction.Command`, :mod:`~pyTooling.CLIAbstraction.Flag` or :class:`~pyTooling.CLIAbstraction.Argument.PathArgument`. .. seealso:: * For path argument. |br| |rarr| :class:`~pyTooling.CLIAbstraction.Argument.PathArgument` List of Strings =============== Like :class:`~pyTooling.CLIAbstraction.Argument.StringArgument` but supporting a list of strings. .. seealso:: * For list of path arguments. |br| |rarr| :class:`~pyTooling.CLIAbstraction.Argument.PathListArgument` Path ==== An argument accepting a :class:`~pathlib.Path` object. List of Paths ============= Like :class:`~pyTooling.CLIAbstraction.Argument.PathArgument` but supporting a list of paths. .. _CLIABS/Arguments/WithoutPrefix: With Prefix Character(s) ************************ Commonly used prefix characters are: single and double dash, single slash, or plus character(s). +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Single Dash Argument Format** | **Double Dash Argument Format** | **Single Slash Argument Format** | **Argument Class** | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``-command`` | ``--command`` | ``/command`` | :class:`~pyTooling.CLIAbstraction.Command.ShortCommand` |br| :class:`~pyTooling.CLIAbstraction.Command.LongCommand` |br| :class:`~pyTooling.CLIAbstraction.Command.WindowsCommand` | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``-flag`` | ``--flag`` | ``/flag`` | :class:`~pyTooling.CLIAbstraction.Flag.ShortFlag` |br| :class:`~pyTooling.CLIAbstraction.Flag.LongFlag` |br| :class:`~pyTooling.CLIAbstraction.Flag.WindowsFlag` | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``-flag`` |br| ``-no-flag`` | ``--flag`` |br| ``--no-flag`` | ``/flag`` |br| ``/no-flag`` | :class:`~pyTooling.CLIAbstraction.BooleanFlag.ShortBooleanFlag` |br| :class:`~pyTooling.CLIAbstraction.BooleanFlag.LongBooleanFlag` |br| :class:`~pyTooling.CLIAbstraction.BooleanFlag.WindowsBooleanFlag` | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``-flag`` |br| ``-flag=value`` | ``--flag`` |br| ``--flag=value`` | ``/flag`` |br| ``/flag=value`` | :class:`~pyTooling.CLIAbstraction.OptionalValuedFlag.ShortOptionalValuedFlag` |br| :class:`~pyTooling.CLIAbstraction.OptionalValuedFlag.LongOptionalValuedFlag` |br| :class:`~pyTooling.CLIAbstraction.OptionalValuedFlag.WindowsOptionalValuedFlag` | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``-flag=value`` | ``--flag=value`` | ``/flag=value`` | :class:`~pyTooling.CLIAbstraction.ValuedFlag.ShortValuedFlag` |br| :class:`~pyTooling.CLIAbstraction.ValuedFlag.LongValuedFlag` |br| :class:`~pyTooling.CLIAbstraction.ValuedFlag.WindowsValuedFlag` | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``-flag=value1 -flag=value2`` | ``--flag=value1 --flag=value2`` | ``/flag=value1 /flag=value2`` | :class:`~pyTooling.CLIAbstraction.ValuedFlagList.ShortValuedFlagList` |br| :class:`~pyTooling.CLIAbstraction.ValuedFlagList.LongValuedFlagList` |br| :class:`~pyTooling.CLIAbstraction.ValuedFlagList.WindowsValuedFlagList` | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``-flag value`` | ``--flag value`` | ``/flag value`` | :class:`~pyTooling.CLIAbstraction.ShortTupleFlag` |br| :class:`~pyTooling.CLIAbstraction.LongTupleFlag` |br| :class:`~pyTooling.CLIAbstraction.WindowsTupleFlag` | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``-gKey1=value1 -gKey2=value2`` | ``--gKey1=value1 --gKey2=value2`` | ``/g:Key1=value1 /g:Key2=value2`` | :class:`~pyTooling.CLIAbstraction.KeyValueFlag.ShortKeyValueFlag` |br| :class:`~pyTooling.CLIAbstraction.KeyValueFlag.LongKeyValueFlag` |br| :class:`~pyTooling.CLIAbstraction.KeyValueFlag.WindowsKeyValueFlag` | +-----------------------------------+-------------------------------------+-----------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ Command ======= .. TODO:: Write documentation. .. mermaid:: graph LR; CLA[CommandLineArgument] style CLA stroke-dasharray: 5 5 CLA --> NA[NamedArgument] style NA stroke-dasharray: 5 5 NA --> CA["CommandArgument
command
"]; CA --> SCA["ShortCommand
-command
"]; CA --> LCA["LongCommand
--command
"]; CA --> WCA["WindowsCommand
/command
"]; Flag ==== A flag is a command line argument that is either present or not. If present that argument is said to be activated or true. 3 variants are predefined with prefixes ``-``, ``--`` and ``/``. .. rubric:: Variants .. mermaid:: graph LR; CLA[CommandLineArgument] style CLA stroke-dasharray: 5 5 CLA --> NA[NamedArgument] style NA stroke-dasharray: 5 5 NA --> FA[FlagArgument] style FA stroke-dasharray: 5 5 FA --> SFA["ShortFlag
-flag
"] FA --> LFA["LongFlag
--flag
"] FA --> WFA["WindowsFlag
/flag
"] Flag with Value =============== .. TODO:: Write documentation. Boolean Flag ============ .. TODO:: Write documentation. Flag with Optional Value ======================== .. TODO:: Write documentation. List of Flags with Value ======================== .. TODO:: Write documentation. Flag with Value as a Tuple ========================== .. TODO:: Write documentation. pyTooling-8.11.0/doc/CLIAbstraction/Executable.rst000066400000000000000000000025121513317154500217530ustar00rootroot00000000000000.. _CLIABS/Executable: Executable ########## The :class:`~pyTooling.CLIAbstraction.Executable` is derived from :class:`~pyTooling.CLIAbstraction.Program`, which represents an executable command line program. In addition, it offers an API to :class:`subprocess.Popen`, so an abstracted command line program can be launched. **Features:** * Launch an abstracted CLI program using :class:`subproess.Popen`. * Setup and modify the environment for the launched program. * Provide a line-based STDOUT reader as generator. Simple Example ************** The following example implements a portion of the ``git`` program and its ``--version`` argument. .. rubric:: Program Definition .. code-block:: Python :name: EXEC:Example:Definition :caption: Git program defining --version argument. class Git(Executable): _executableNames: ClassVar[Dict[str, str]] = { "Darwin": "git", "FreeBSD": "git", "Linux": "git", "Windows": "git.exe" } @CLIArgument() class FlagVersion(LongFlag, name="version"): """Print the version information.""" .. rubric:: Program Usage .. code-block:: Python :name: EXEC:Example:Usage :caption: Usage of the abstracted Git program. git = Git() git[git.FlagVersion] = True git.StartProcess() for line in git.GetLineReader(): print(line) pyTooling-8.11.0/doc/CLIAbstraction/Program.rst000066400000000000000000000037511513317154500213070ustar00rootroot00000000000000.. _CLIABS/Program: Program ####### The :class:`~pyTooling.CLIAbstraction.Program` represents an executable command line program. It offers an interface to define and enable command line arguments. **Features:** * Abstract a command line program as a Python class. * Abstract arguments of that program as nested classes derived from pre-defined Argument classes. |br| See :ref:`CLIABS/Arguments`. * Construct a list of arguments in correct order and with proper escaping ready to be used with e.g. :mod:`subprocess`. Simple Example ************** The following example implements a portion of the ``git`` program and its ``--version`` argument. .. rubric:: Program Definition .. code-block:: Python :name: PROG:Example:Definition :caption: Git program defining --version argument. class Git(Program): _executableNames: ClassVar[Dict[str, str]] = { "Darwin": "git", "FreeBSD": "git", "Linux": "git", "Windows": "git.exe" } @CLIArgument() class FlagVersion(LongFlag, name="version"): """Print the version information.""" .. rubric:: Program Usage .. code-block:: Python :name: PROG:Example:Usage :caption: Usage of the abstracted Git program. git = Git() git[git.FlagVersion] = True print(git.AsArgument()) Setting Program Names based on OS ********************************* .. todo:: * set executable name based on the operating system. Defining Arguments on a Program ******************************* .. todo:: * use decorator ``CLIArgument`` * usage of nested classes * parametrize nested classes with class-arguments .. _CLIABS/CLIArgument: CLIArgument =========== CLIArgument attribute Setting Arguments on a Program ****************************** .. todo:: * Using dictionary syntax with nested classes as typed keys. * Using ``Value`` to change the arguments value at runtime. Derive Program Variants *********************** .. todo:: * Explain helper methods to copy active arguments. pyTooling-8.11.0/doc/CLIAbstraction/index.rst000066400000000000000000000141001513317154500207750ustar00rootroot00000000000000.. _CLIABS: Overview ######## :mod:`~pyTooling.CLIAbstraction` offers an abstraction layer for command line programs, so they can be used easily in Python. There is no need for manually assembling parameter lists or considering the order of parameters. All parameters like ``-v`` or ``--value=42`` are described as :class:`~pyTooling.CLIAbstraction.Argument.CommandLineArgument` instances on a :class:`~pyTooling.CLIAbstraction.Program` class. Each argument class like :class:`~pyTooling.CLIAbstraction.Flag.ShortFlag` or :class:`~pyTooling.CLIAbstraction.Argument.PathArgument` knows about the correct formatting pattern, character escaping, and if needed about necessary type conversions. A program instance can be converted to an argument list suitable for :class:`subprocess.Popen`. While a user-defined command line program abstraction derived from :class:`~pyTooling.CLIAbstraction.Program` only takes care of maintaining and assembling parameter lists, a more advanced base-class, called :class:`~pyTooling.CLIAbstraction.Executable`, is offered with embedded :class:`~subprocess.Popen` behavior. .. _CLIABS/Goals: Design Goals ************ The main design goals are: * Offer access to CLI programs as Python classes. * Abstract CLI arguments (a.k.a. parameter, option, flag, ...) as members on such a Python class. * Abstract differences in operating systems like argument pattern (POSIX: ``-h`` vs. Windows: ``/h``), path delimiter signs (POSIX: ``/`` vs. Windows: ``\``) or executable names. * Derive program variants from existing programs. * Assemble parameters as list for handover to :class:`subprocess.Popen` with proper escaping and quoting. * Launch a program with :class:`~subprocess.Popen` and hide the complexity of Popen. * Get a generator object for line-by-line output reading to enable postprocessing of outputs. .. _CLIABS/Example: Example ******* The following example implements a portion of the ``git`` program and its ``commit`` sub-command. 1. A new class ``Git`` is derived from :class:`pyTooling.CLIAbstraction.Executable`. 2. A class variable ``_executableNames`` is set, to specify different executable names based on the operating system. 3. Nested classes are used to describe arguments and flags for the Git program. 4. These nested classes are annotated with the ``@CLIArgument`` attribute, which is used to register the nested classes in an ordered lookup structure. This declaration order is also used to order arguments when converting to a list for :class:`~subprocess.Popen`. .. grid:: 2 .. grid-item:: **Usage of** ``Git`` :columns: 6 .. code-block:: Python # Create a program instance and set common parameters. git = Git() git[git.FlagVerbose] = True # Derive a variant of that pre-configured program. commit = git.getCommitTool() commit[commit.ValueCommitMessage] = "Bumped dependencies." # Launch the program and parse outputs line-by-line. commit.StartProcess() for line in commit.GetLineReader(): print(line) .. grid-item:: **Declaration of** ``Git`` :columns: 6 .. code-block:: Python from pyTooling.CLIAbstraction import Executable from pyTooling.CLIAbstraction.Command import CommandArgument from pyTooling.CLIAbstraction.Flag import LongFlag from pyTooling.CLIAbstraction.ValuedTupleFlag import ShortTupleFlag class Git(Executable): _executableNames: ClassVar[Dict[str, str]] = { "Darwin": "git", "FreeBSD": "git", "Linux": "git", "Windows": "git.exe" } @CLIArgument() class FlagVerbose(LongFlag, name="verbose"): """Print verbose messages.""" @CLIArgument() class CommandCommit(CommandArgument, name="commit"): """Command to commit staged files.""" @CLIArgument() class ValueCommitMessage(ShortTupleFlag, name="m"): """Specify the commit message.""" def GetCommitTool(self): """Derive a new program from a configured program.""" tool = self.__class__(executablePath=self._executablePath) tool[tool.CommandCommit] = True self._CopyParameters(tool) return tool .. _CLIABS/ProgramAPI: Programm API ************ **Condensed definition of class** :class:`~pyTooling.CLIAbstraction.Program`: .. code-block:: Python class Program(metaclass=ExtendedType, slots=True): # Register @CLIArgument marked nested classes in `__cliOptions__ def __init_subclass__(cls, *args: Tuple[Any, ...], **kwargs: Dict[str, Any]): ... def __init__(self, executablePath: Path = None, binaryDirectoryPath: Path = None, dryRun: bool = False) -> None: ... @staticmethod def _NeedsParameterInitialization(key): ... # Implement indexed access operators: prog[...] def __getitem__(self, key): ... def __setitem__(self, key, value): ... @readonly def Path(self) -> Path: ... def ToArgumentList(self) -> List[str]: ... def __repr__(self): ... def __str__(self): ... .. _CLIABS/ExecutableAPI: Executable API ************** **Condensed definition of class** :class:`~pyTooling.CLIAbstraction.Executable`: .. code-block:: Python class Executable(Program): def __init__( self, executablePath: Path = None, binaryDirectoryPath: Path = None, workingDirectory: Path = None, # environment: Environment = None, dryRun: bool = False): ... def StartProcess(self): ... def Send(self, line: str, end: str="\n") -> None: ... def GetLineReader(self) -> Generator[str, None, None]: ... @readonly def ExitCode(self) -> int: ... .. _CLIABS/Consumers: Consumers ********* This abstraction layer is used by: * ✅ Wrap command line interfaces of EDA tools (Electronic Design Automation) in Python classes. |br| `pyEDAA.CLITool `__ pyTooling-8.11.0/doc/CodeCoverage.rst000066400000000000000000000010271513317154500174170ustar00rootroot00000000000000Code Coverage Summary ##################### .. grid:: 2 .. grid-item:: :columns: 8 .. report:code-coverage:: :reportid: src .. grid-item:: :columns: 4 .. report:code-coverage-legend:: :reportid: src :style: vertical-table ---------- Code coverage report generated with `pytest `__, `Coverage.py `__ and visualized by `sphinx-reports `__. pyTooling-8.11.0/doc/Common/000077500000000000000000000000001513317154500155675ustar00rootroot00000000000000pyTooling-8.11.0/doc/Common/CallByRef.rst000066400000000000000000000060661513317154500201340ustar00rootroot00000000000000.. _COMMON/CallByRef: CallByRef ######### The :mod:`pyTooling.CallByRef` package contains auxiliary classes to implement *call-by-reference* emulation for function parameter handover. The callee gets enabled to return out-parameters for simple types like :class:`bool` and :class:`int` to the caller. .. #contents:: Table of Contents :local: :depth: 2 By implementing a wrapper-class :class:`~pyTooling.CallByRef.CallByRefParam`, any type's value can be passed by-reference. In addition, standard types like :class:`int` or :class:`bool` can be handled by derived wrapper-classes. .. admonition:: Python Background Python does not allow a user to distinguish between *call-by-value* and *call-by-reference* parameter passing. Python's standard types are passed by-value to a function or method. Instances of a class are passed by-reference (pointer) to a function or method. .. rubric:: Inheritance diagram: .. inheritance-diagram:: pyTooling.CallByRef :parts: 1 .. admonition:: Example .. code-block:: Python from pyTooling.CallByRef import CallByRefIntParam # define a call-by-reference parameter for integer values myInt = CallByRefIntParam(3) # a function using a call-by-reference parameter def func(param : CallByRefIntParam): param <<= param * 4 # call the function and pass the wrapper object func(myInt) print(myInt.value) .. _COMMON/CallByRefParam: CallByRefParam ************** :class:`~pyTooling.CallByRef.CallByRefParam` implements a wrapper class for an arbitrary *call-by-reference* parameter that can be passed to a function or method. The parameter can be initialized via the constructor. If no init-value was given, the init value will be ``None``. The wrappers internal value can be updated by using the inplace shift-left operator ``<=``. In addition, operators ``=`` and ``!=`` are also implemented for any *call-by-reference* wrapper. Calls to ``__repr__`` and ``__str__`` are passed to the internal value. The internal value can be used via ``obj.value``. Type-Specific *call-by-reference* Classes ***************************************** .. _COMMON/CallByRefBoolParam: CallByRefBoolParam ================== The class :class:`~pyTooling.CallByRef.CallByRefBoolParam` implements call-by-ref behavior for the boolean type (:class:`bool`). Implemented operators: * Binary comparison operators: ``==``, ``!=`` * Type conversions: ``bool()``, ``int()`` .. _COMMON/CallByRefIntParam: CallByRefIntParam ================= The class :class:`~pyTooling.CallByRef.CallByRefIntParam` implements call-by-ref behavior for the integer type (:class:`int`). Implemented operators: * Unary operators: ``+``, ``-``, ``~`` * Binary boolean operators: ``&``, ``|``, ``^`` * Binary arithmetic operators: ``+``, ``-``, ``*``, ``/``, ``//``, ``%``, ``**`` * Binary comparison operators: ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=`` * Inplace boolean operators: ``&=``, ``|=``, ``^=`` * Inplace arithmetic operators: ``+=``, ``-=``, ``*=``, ``/=``, ``//=``, ``%=``, ``**=`` * Type conversions: ``bool()``, ``int()`` pyTooling-8.11.0/doc/Common/Filesystem.rst000066400000000000000000000037501513317154500204520ustar00rootroot00000000000000.. _FILESYS: Filesystem ########## The :mod:`pyTooling.Filesystem` package provides fast and simple access to directory statistics like file sizes, accumulated directory sizes, symlinks, hardlinks, etc. .. _FILESYS/Features: Features ******** * Scan a directory and its subdirectories for files and create a in-memory filesystem view (directories, files, symbolic links, hard links). * Identify filenames pointing to the same file (a.k.a hard links). * Compute directory sizes by aggregating file sizes. .. _FILESYS/MissingFeatures: Missing Features ================ * tbd .. _FILESYS/PlannedFeatures: Planned Features ================ * tbd .. _FILESYS/RejectedFeatures: Out of Scope ============ * tbd .. _FILESYS/ByFeature: By Feature ********** .. danger:: Accessing internal fields of a node is strongly not recommended for users, as it might lead to a corrupted tree data structure. If a power-user wants to access these fields, feel free to use them for achieving a higher performance, but you got warned 😉. .. _FILESYS/Root: Root Reference ============== tbd .. _FILESYS/Parent: Parent Reference ================ tbd .. _FILESYS/Size: Size ==== tbd .. _FILESYS/Competitors: Competing Solutions ******************* .. _FILESYS/Directory-Tree: Directory Tree ============== Source: :gh:`Directory Tree ` .. todo:: FILESYS::Directory-Tree write comparison here. .. rubric:: Disadvantages * ... .. rubric:: Standoff * ... .. rubric:: Advantages * ... .. _FILESYS/folderstats: folderstats =========== Source: :gh:`folderstats ` .. todo:: FILESYS::folderstats write comparison here. .. rubric:: Disadvantages * ... .. rubric:: Standoff * ... .. rubric:: Advantages * ... .. _FILESYS/dutree: dutree ====== Source: :gh:`dutree ` .. todo:: FILESYS::dutree write comparison here. .. rubric:: Disadvantages * ... .. rubric:: Standoff * ... .. rubric:: Advantages * ... pyTooling-8.11.0/doc/Common/Licensing.rst000066400000000000000000000143211513317154500202350ustar00rootroot00000000000000.. _LICENSING: Licensing ######### The :mod:`pyTooling.Licensing` package provides auxiliary classes to represent commonly known licenses and mappings of their names, because some tools use differing names for the same license. .. #contents:: Table of Contents :local: :depth: 1 .. admonition:: Background Information There are several names, identifiers and (Python package) classifiers referring to the same license. E.g. package classifiers used by setuptools and displayed by PIP/PyPI are different from SPDX identifiers and sometimes they are not even identical to the official license names. Also some allegedly similar licenses got different SPDX identifiers. The package :mod:`pyTooling.Licensing` provides license name and identifiers mappings to unify all these names and classifiers to and from `SPDX identifiers `__. .. rubric:: Examples: +------------------+------------------------------+--------------------------+--------------------------------------------------------+ | SDPX Identifier | Official License Name | License (short) Name | Python package classifier | +==================+==============================+==========================+========================================================+ | ``Apache-2.0`` | Apache License, Version 2.0 | ``Apache 2.0`` | ``License :: OSI Approved :: Apache Software License`` | +------------------+------------------------------+--------------------------+--------------------------------------------------------+ | ``BSD-3-Clause`` | The 3-Clause BSD License | ``BSD`` | ``License :: OSI Approved :: BSD License`` | +------------------+------------------------------+--------------------------+--------------------------------------------------------+ .. _LICENSING/License: Licenses ******** The :class:`~pyTooling.Licensing.License` class represents of a license like *Apache License, Version 2.0* (SPDX: ``Apache-2.0``). It offers several information about a license as properties. Licenses can be compared for equality (``==``, ``!=``) based on there SPDX identifier. **Condensed definition of class** :class:`~pyTooling.Licensing.License`: .. code-block:: python @export class License(metaclass=ExtendedType, slots=True): def __init__(self, spdxIdentifier: str, name: str, osiApproved: bool = False, fsfApproved: bool = False) -> None: @property def Name(self) -> str: @property def SPDXIdentifier(self) -> str: @property def OSIApproved(self) -> bool: @property def FSFApproved(self) -> bool: @property def PythonLicenseName(self) -> str: @property def PythonClassifier(self) -> str: def __eq__(self, other: Any) -> bool: def __ne__(self, other: Any) -> bool: # def __le__(self, other: Any) -> bool: # def __ge__(self, other: Any) -> bool: def __repr__(self) -> str: def __str__(self) -> str: The licenses supported by this package are available as individual package variables. Package variables of predefined licenses: * :data:`~pyTooling.Licensing.Apache_2_0_License` * :data:`~pyTooling.Licensing.BSD_3_Clause_License` * :data:`~pyTooling.Licensing.GPL_2_0_or_later` * :data:`~pyTooling.Licensing.MIT_License` .. code-block:: python from pyTooling.Licensing import Apache_2_0_License license = Apache_2_0_License print(f"Python classifier: {license.PythonClassifier}") print(f"SPDX: {license.SPDXIdentifier}") # Python classifier: License :: OSI Approved :: Apache Software License # SPDX: Apache-2.0 .. # * :data:`~pyTooling.Licensing.Apache_2_0_License` * :data:`~pyTooling.Licensing.Artistic_License` * :data:`~pyTooling.Licensing.BSD_3_Clause_License` * :data:`~pyTooling.Licensing.BSD_4_Clause_License` * :data:`~pyTooling.Licensing.CreativeCommons_CC0_1_0` * :data:`~pyTooling.Licensing.CreativeCommons_CCBY_4_0` * :data:`~pyTooling.Licensing.CreativeCommons_CCBYSA_4_0` * :data:`~pyTooling.Licensing.EclipsePublicLicense_2_0` * :data:`~pyTooling.Licensing.GNU_AfferoGeneralPublicLicense_3_0` * :data:`~pyTooling.Licensing.GNU_GeneralPublicLicense_2_0_or_later` * :data:`~pyTooling.Licensing.GNU_GeneralPublicLicense_3_0_or_later` * :data:`~pyTooling.Licensing.GNU_LesserGeneralPublicLicense_3_0_or_later` * :data:`~pyTooling.Licensing.MicrosoftPublicLicense` * :data:`~pyTooling.Licensing.MIT_License` * :data:`~pyTooling.Licensing.MozillaPublicLicense_2_0` In addition a dictionary (:data:`~pyTooling.Licensing.SPDX_INDEX`) maps from SPDX identified to :class:`~pyTooling.Licensing.License` instances. .. code-block:: python from pyTooling.License import SPDX_INDEX licenseName = "MIT" license = SPDX_INDEX[licenseName] print(f"Python classifier: {license.PythonClassifier}") print(f"SPDX: {license.SPDXIdentifier}") # Python classifier: License :: OSI Approved :: MIT License # SPDX: MIT .. _LICENSING/Mappings: Mappings ******** :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES` offers a Python specific mapping from SPDX identifier to license names used by Python (setuptools). Each dictionary item contains a :class:`~pyTooling.Licensing.PythonLicenseNames` instance which contains the license name and package classifier used by setuptools. Currently the following licenses are listed in the Python specific name mapping: * Apache-2.0 * BSD-3-Clause * MIT * GPL-2.0-or-later .. _LICENSING/Usage: Usage with Setuptools ********************* The following examples demonstrates the usage with setuptools in a ``setup.py``. .. admonition:: Usage Example .. code-block:: python from setuptools import setup from pyTooling.Licensing import Apache_2_0_License classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only" ] license = Apache_2_0_License classifiers.append(license.PythonClassifier) # Assemble other parameters # ... # Handover to setuptools setup( # ... license=license.SPDXIdentifier, # ... classifiers=classifiers, # ... ) pyTooling-8.11.0/doc/Common/Platform.rst000066400000000000000000000106341513317154500201110ustar00rootroot00000000000000.. _COMMON/Platform: Platform ######## The :class:`~pyTooling.Platform.Platform` class gives detailed platform information about the environment the Python program or script is running in. .. #contents:: Table of Contents :local: :depth: 1 .. admonition:: Background Information Python has several ways in finding out what platform is running underneath of Python. Some information are provided via function calls, others are variables in a module. The :class:`~pyTooling.Platform.Platform` class unifies all these APIs into a single class instance providing access to the platform and runtime information. Moreover, some (older) APIs do not reflect modern platforms like Python running in a MSYS2 MinGW64 environment on a Windows x86-64. By combining multiple APIs, it's possible to identify also such platforms. The internally used APIs are: * :func:`platform.machine` * :data:`sys.platform` * :func:`sysconfig.get_platform` These APIs are currently unused/not needed, because their information is already provided by the ones mentioned above: * :data:`os.name` * :func:`platform.system` * :func:`platform.architecture` .. _COMMON/CurrentPlatform: Current Platform **************** The module variable :data:`pyTooling.Common.CurrentPlatform` contains a singleton instance of :class:`~pyTooling.Platform.Platform`, which abstracts and unifies multiple platform APIs of Python into a single class instance. .. rubric:: Supported platforms * Native * Linux (x86-64) * macOS (x86-64) * Windows (x86-64) * MSYS2 (on Windows) * MSYS * Clang64 * MinGW32 * MinGW64 * UCRT64 * Cygwin .. seealso:: :class:`~pyTooling.Platform.Platform` |rarr| ``Is...`` properties describing a platform (and environment) the current Python program is running on. .. _COMMON/CurrentPlatform/Usecases: Usecase: Platform Specific Code =============================== .. rubric:: Example: .. admonition:: example.py .. code-block:: python from pyTooling.Common import CurrentPlatform # Check for a native Linux platform if CurrentPlatform.IsNativeLinux: pass Usecase: Platform Specific Tests ================================ .. admonition:: unittest.py .. code-block:: python from unittest import TestCase from pytest import mark from pyTooling.Common import CurrentPlatform class MyTestCase(TestCase): @mark.skipif(not CurrentPlatform.IsMinGW64OnWindows, reason="Skipped when platform isn't MinGW64.") def test_OnMinGW64(self) -> None: pass .. _COMMON/Platform/Architectures: Architectures ************* The architectures describes the native bit-width of addresses in a system. Thus, the maximum addressable memory space of a CPU. E.g. a 32-bit architecture can address 4 GiB of main memory without memory segmentation. .. rubric:: Supported Architectures * x86_32 * x86_64 .. code-block:: python from pyTooling.Common import CurrentPlatform # CurrentPlatform.Architecture .. _COMMON/Platform/NativePlatforms: Native Platforms **************** The native platform describes the hosting operating system. .. rubric:: Supported Native Platforms * Linux * macOS * Windows .. code-block:: python from pyTooling.Common import CurrentPlatform # Check if the platform is a native platform CurrentPlatform.IsNativePlatform # Check for native Windows CurrentPlatform.IsNativeWindows # Check for native Linux CurrentPlatform.IsNativeLinux # Check for native macOS CurrentPlatform.IsNativeMacOS .. _COMMON/Platform/Environments: Environments ************ An environment is an additional layer installed on an operating system that provides a runtime environment to execute Python. E.g. the ``MSYS2`` environment provides ``MinGW64`` to run Python in a Linux like POSIX environment, but on top of Windows. .. rubric:: Supported Environments * MSYS2 * Cygwin .. code-block:: python from pyTooling.Common import CurrentPlatform # Check if the environment is MSYS2 CurrentPlatform.IsMSYS2Environment .. _COMMON/Platform/Runtimes: Runtimes ******** Some environments like ``MSYS2`` provide multiple runtimes. .. rubric:: Supported (MSYS2) Runtimes * MSYS * MinGW32 * MinGW64 * UCRT64 * (CLang32) * CLang64 .. code-block:: python from pyTooling.Common import CurrentPlatform # Check if the runtime is MSYS2 MinGW64 on a Windows machine CurrentPlatform.IsMinGW64OnWindows pyTooling-8.11.0/doc/Common/Stopwatch.rst000066400000000000000000000121131513317154500202730ustar00rootroot00000000000000.. _COMMON/Stopwatch: Stopwatch ######### .. #contents:: Table of Contents :depth: 1 .. grid:: 2 .. grid-item:: :columns: 6 The stopwatch implements a solution to measure and collect timings: e.g. code execution times or test run times. The time measurement can be :meth:`started `, :meth:`paused `, :meth:`resumed ` and :meth:`stopped `. More over, split times can be taken too. The measurement is based on :func:`time.perf_counter_ns`. Additionally, starting and stopping is preserved as absolute time via :meth:`datetime.datetime.now`. Every split time taken is a time delta to the previous stopwatch operation. These are preserved in an internal sequence of splits. This sequence includes time deltas of activity and inactivity. Thus, a running stopwatch can be split as well as a paused stopwatch. The stopwatch can also be used in a :ref:`with-statement `, because it implements the :ref:`context manager protocol `. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Start/Stop .. code-block:: Python from pyTooling.Stopwatch import Stopwatch sw = Stopwatch("my name") sw.Start() # do something sw.Stop() sw = Stopwatch("other name", started=True) # do something sw.Stop() .. tab-item:: Start/Pause/Resume/Stop .. code-block:: Python from pyTooling.Stopwatch import Stopwatch sw = Stopwatch("my name") sw.Start() # do something sw.Pause() # do something other sw.Resume() # do something again sw.Stop() .. tab-item:: Using with-statement .. code-block:: Python from pyTooling.Stopwatch import Stopwatch sw = Stopwatch("my name", preferPause=True) with sw: # do something # do something other with sw # do something again .. _COMMON/Stopwatch/Features: Features ******** .. grid:: 2 .. grid-item:: :columns: 6 Name A stopwatch can be named at creation time. Starting and stopping The stopwatch can be started and stopped. Once stopped, no further start or pause/resume is possible. A stopwatch can't be restarted. A new stopwatch object should be created and the old can be destroyed. The stopwatch collects the absolute start (begin) and stop (end) times. It then provides a duration from start to stop operation. Pause and resume A stopwatch can be paused and resumed. Split times tbd Iterating split times tbd Using in a ``with``-statement tbd State of a stopwatch tbd .. grid-item:: :columns: 6 .. code-block:: Python @export class Stopwatch(SlottedObject): def __init__(self, name: str = None, started: bool = False, preferPause: bool = False) -> None: ... def __enter__(self) -> "Stopwatch": ... def __exit__(self, exc_type: Type[Exception], exc_val: Exception, exc_tb: Traceback) -> bool: ... def Start(self) -> None: ... def Split(self) -> float: ... def Pause(self) -> float: ... def Resume(self) -> float: ... def Stop(self): ... @readonly def Name(self) -> Nullable[str]: ... @readonly def IsStarted(self) -> bool: ... @readonly def IsRunning(self) -> bool: ... @readonly def IsPaused(self) -> bool: ... @readonly def IsStopped(self) -> bool: ... @readonly def StartTime(self) -> Nullable[datetime]: ... @readonly def StopTime(self) -> Nullable[datetime]: ... @readonly def HasSplitTimes(self) -> bool: ... @readonly def SplitCount(self) -> int: ... @readonly def ActiveCount(self) -> int: ... @readonly def InactiveCount(self) -> int: ... @readonly def Activity(self) -> float: ... @readonly def Inactivity(self) -> float: ... @readonly def Duration(self) -> float: ... def __len__(self): ... def __getitem__(self, index: int) -> Tuple[float, bool]: ... def __iter__(self) -> Iterator[Tuple[float, bool]]: ... pyTooling-8.11.0/doc/Common/Versioning.rst000066400000000000000000000607541513317154500204600ustar00rootroot00000000000000.. _VERSIONING: Versioning ########## The :mod:`pyTooling.Versioning` package provides auxiliary classes to implement :ref:`VERSIONING/SemanticVersion` (following `SemVer `__ rules) and :ref:`VERSIONING/CalendarVersion` (following `CalVer `__ rules). The latter one has multiple variants due to the meaning of the version's parts like: year-month version or year-week version. Versions can be grouped by :ref:`version sets ` and :ref:`version ranges `. .. _VERSIONING/SemanticVersion: Semantic Versioning ******************* The :class:`~pyTooling.Versioning.SemanticVersion` class represents of a version number like ``v3.7.12``. It consists of a major, minor and micro number. The micro number is also known as patch number. The minor and micro numbers are optional, but usually used by most semantic version numbering schemes. In addition, optional parts can be added like a prefix, a postfix or a build number. .. hint:: Given a version number ``MAJOR.MINOR.MICRO``, increment the: * ``MAJOR`` version when you make incompatible API changes, * ``MINOR`` version when you add functionality in a backwards compatible manner, and * ``MICRO`` version when you make backwards compatible bug fixes. * Additional labels for pre-release and build metadata are available as extensions to the ``MAJOR.MINOR.MICRO`` format. Summary taken from `semver.org `__. .. grid:: 2 .. grid-item:: :columns: 6 .. rubric:: Direct Instantiation A semantic version can be constructed from parts like major, minor and micro numbers. .. code-block:: python # Construct from numbers version = SemanticVersion(1, 5, 2) .. rubric:: Construction from String Alternatively, a semantic version can be created from a string containing a semantic version number by using the class-method :meth:`~pyTooling.Versioning.SemanticVersion.Parse`. The string is parsed and a semantic version gets returned. .. code-block:: python # Construct from string version = SemanticVersion.Parse("0.22.8") .. rubric:: Usage .. code-block:: python # Compare versions if version2 > version1: # Compare versions if version2 >= "1.4.8": .. rubric:: Features Prefix string Represents the prefix like: ``v`` (version), ``r`` (revision), ``i`` (internal version/release), ``ver`` (version), ``rev`` (revision). :green:`v`\ 1.2.3 Major number Represents the major version number in semantic version. v\ :green:`1`\ .2.3 Minor number Represents the minor version number in semantic version. v1.\ :green:`2`\ .3 Micro number Represents the micro or patch version number in semantic version. v1.2.\ :green:`3` Build number Represents the build number. v1.2.3.\ :green:`4` Release Level / Release number Distinguishes if a version is in *alpha*, *beta*, *release candidate* or *final* release level. v1.2.3.\ :green:`alpha4` |br| v1.2.3.\ :green:`beta4` |br| v1.2.3.\ :green:`rc4` Post number tbd v1.2.3.\ :green:`post4` Development number tbd v1.2.3.\ :green:`dev4` Postfix string v1.2.3+\ :green:`deb11u5` Comparison operators Operators for ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, ``>>``. String formatting The version number can be formatted as a string with a fixed formatting pattern based on present version parts as well as a user-defined formatting via :meth:`~pyTooling.Versioning.SemanticVersion.__format__` .. rubric:: Examples .. hlist:: :columns: 3 * ``v1`` * ``r1.12`` * ``i1.2.13+linux_86_64`` * ``rev1.2.3.14`` * ``v1.2.3-dev`` * ``v1.2.3.dev23`` * ``v1.2.3.alpha1`` * ``v1.2.3.beta1`` * ``v1.2.3.rc1+deb25`` * ``1.2.8.post2`` * ``1.2.8.post2.dev4`` * ``v1.2.3.alpha4.post5.dev6+deb11u35`` .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: Python @export class SemanticVersion(Version): @classmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "Version": pass @readonly def Parts(self) -> Parts: pass @readonly def Prefix(self) -> str: pass @readonly def Major(self) -> int: pass @readonly def Minor(self) -> int: pass @readonly def Micro(self) -> int: pass @readonly def Patch(self) -> int: pass @readonly def ReleaseLevel(self) -> ReleaseLevel: pass @readonly def ReleaseNumber(self) -> int: pass @readonly def Post(self) -> int: pass @readonly def Dev(self) -> int: pass @readonly def Build(self) -> int: pass @readonly def Postfix(self) -> str: pass @readonly def Hash(self) -> str: pass @readonly def Flags(self) -> Flags: pass def __eq__(self, other: Union["SemanticVersion", str, int, None]) -> bool: pass def __ne__(self, other: Union["SemanticVersion", str, int, None]) -> bool: pass def __lt__(self, other: Union["SemanticVersion", str, int, None]) -> bool: pass def __le__(self, other: Union["SemanticVersion", str, int, None]) -> bool: pass def __gt__(self, other: Union["SemanticVersion", str, int, None]) -> bool: pass def __ge__(self, other: Union["SemanticVersion", str, int, None]) -> bool: pass def __imod__(self, other: Union["SemanticVersion", str, int, None]) -> bool: pass def __format__(self, formatSpec: str) -> str: pass def __repr__(self) -> str: pass def __str__(self) -> str: pass .. _VERSIONING/SemVerVariants: Variants ======== .. tab-set:: .. tab-item:: Python Version .. grid:: 2 .. grid-item:: :columns: 6 .. rubric:: Examples * 3.13.0 * 3.13.0a4 * 3.13.0b2 * 3.13.0rc2 .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: Python @export class PythonVersion(SemanticVersion): @classmethod def FromSysVersionInfo(cls) -> "PythonVersion": pass .. _VERSIONING/CalendarVersion: Calendar Versioning ******************* The :class:`~pyTooling.Versioning.CalendarVersion` class represents of a version number like ``2021.10``. .. grid:: 2 .. grid-item:: :columns: 6 .. rubric:: Direct Instantiation Alternatively, a calendar version can be constructed from parts like major, minor and micro numbers. The unified naming of parts can be used to map years to major numbers, months to minor numbers, etc. .. code-block:: python # Construct from numbers version = CalendarVersion(2024, 5) .. rubric:: Construction from String A calendar version can be created from a string containing a calendar version number by using the class-method :meth:`~pyTooling.Versioning.CalendarVersion.Parse`. The string is parsed and a calendar version gets returned. .. code-block:: python # Construct from string version = CalendarVersion.Parse("2024.05") .. rubric:: Usage .. code-block:: python # Compare versions if version2 > version1: # Compare versions if version2 >= "2023.02": .. rubric:: Features Major number Represents the major version number in semantic version. Minor number Represents the minor version number in semantic version. Micro number Represents the micro or patch version number in semantic version. Build number Represents the build number. Prefix string Represents the prefix like: ``v`` (version), ``r`` (revision), ``i`` (internal version/release), ``ver`` (version), ``rev`` (revision). Comparison operators Operators for ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, ``>>``. .. rubric:: Missing Features * release-level: additional labels like ``dev``, ``rc``, ``pl``, ``alpha`` * pre-version and post-version .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: Python @export class CalendarVersion(Version): @classmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["CalendarVersion"], bool]] = None) -> "CalendarVersion": pass @readonly def Parts(self) -> Parts: pass @readonly def Major(self) -> int: pass @readonly def Minor(self) -> int: pass @readonly def Micro(self) -> int: pass @readonly def Patch(self) -> int: pass @readonly def Build(self) -> int: pass @readonly def Flags(self) -> Flags: pass @readonly def Prefix(self) -> str: pass @readonly def Postfix(self) -> str: pass def __eq__(self, other: Union["CalendarVersion", str, int, None]) -> bool: pass def __ne__(self, other: Union["CalendarVersion", str, int, None]) -> bool: pass def __lt__(self, other: Union["CalendarVersion", str, int, None]) -> bool: pass def __le__(self, other: Union["CalendarVersion", str, int, None]) -> bool: pass def __gt__(self, other: Union["CalendarVersion", str, int, None]) -> bool: pass def __ge__(self, other: Union["CalendarVersion", str, int, None]) -> bool: pass def __imod__(self, other: Union["CalendarVersion", str, int, None]) -> bool: pass def __format__(self, formatSpec: str) -> str: pass def __repr__(self) -> str: pass def __str__(self) -> str: pass .. _VERSIONING/CalVerVariants: Variants ======== .. hint:: Calendar versions have multiple format variants: * ``YY.MINOR.MICRO`` * ``YYYY.MINOR.MICRO`` * ``YY.MM`` * ``YYYY.0M`` * ``YYYY.MM.DD`` * ``YYYY.MM.DD_MICRO`` * ``YYYY-MM-DD`` Formats taken from `calver.org `__. .. tab-set:: .. tab-item:: Year-Month Version .. grid:: 2 .. grid-item:: :columns: 6 .. rubric:: Direct Instantiation A year-month version can be constructed from year and month numbers. .. code-block:: python # Construct from numbers version = YearMonthVersion(2024, 5) .. rubric:: Construction from String A semantic version can also be created from a string containing a year-month version number by using the class-method :meth:`~pyTooling.Versioning.YearMonthVersion.Parse`. The string is parsed and a year-month version gets returned. .. code-block:: python # Construct from string version = YearMonthVersion.Parse("2024.05") .. rubric:: Examples * OSVVM: 2024.07 * Ubuntu: 2024.10 .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: Python @export class YearMonthVersion(CalendarVersion): @classmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["YearMonthVersion"], bool]] = None) -> "YearMonthVersion": pass @readonly def Year(self) -> int: pass @readonly def Month(self) -> int: pass .. tab-item:: Year-Week Version .. grid:: 2 .. grid-item:: :columns: 6 .. rubric:: Direct Instantiation A year-week version can be constructed from year and month numbers. .. code-block:: python # Construct from numbers version = YearWeekVersion(2024, 5) .. rubric:: Construction from String A semantic version can also be created from a string containing a year-week version number by using the class-method :meth:`~pyTooling.Versioning.YearWeekVersion.Parse`. The string is parsed and a year-week version gets returned. .. code-block:: python # Construct from string version = YearWeekVersion.Parse("2024.05") .. rubric:: Examples * Production date codes .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: Python @export class YearWeekVersion(CalendarVersion): @classmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["YearWeekVersion"], bool]] = None) -> "YearWeekVersion": pass @readonly def Year(self) -> int: pass @readonly def Week(self) -> int: pass .. tab-item:: Year-Release Version .. grid:: 2 .. grid-item:: :columns: 6 .. rubric:: Direct Instantiation A year-release version can be constructed from year and month numbers. .. code-block:: python # Construct from numbers version = YearReleaseVersion(2024, 2) .. rubric:: Construction from String A semantic version can also be created from a string containing a year-release version number by using the class-method :meth:`~pyTooling.Versioning.YearReleaseVersion.Parse`. The string is parsed and a year-release version gets returned. .. code-block:: python # Construct from string version = YearReleaseVersion.Parse("2024.2") .. rubric:: Examples * Vivado: 2024.1 .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: Python @export class YearReleaseVersion(CalendarVersion): @classmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["YearReleaseVersion"], bool]] = None) -> "YearReleaseVersion": pass @readonly def Year(self) -> int: pass @readonly def Release(self) -> int: pass .. tab-item:: Year-Month-Day Version .. grid:: 2 .. grid-item:: :columns: 6 .. rubric:: Direct Instantiation A year-month-day version can be constructed from year, month and day numbers. .. code-block:: python # Construct from numbers version = YearMonthDayVersion(2024, 10, 5) .. rubric:: Construction from String A semantic version can also be created from a string containing a year-month-day version number by using the class-method :meth:`~pyTooling.Versioning.YearMonthDayVersion.Parse`. The string is parsed and a year-month-day version gets returned. .. code-block:: python # Construct from string version = YearMonthDayVersion.Parse("2024.10.05") .. rubric:: Examples * Furo: 2024.04.27 .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: Python @export class YearMonthDayVersion(CalendarVersion): @classmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["YearMonthDayVersion"], bool]] = None) -> "YearMonthDayVersion": pass @readonly def Year(self) -> int: pass @readonly def Month(self) -> int: pass @readonly def Day(self) -> int: pass .. _VERSIONING/VersionRange: VersionRange ************ .. grid:: 2 .. grid-item:: :columns: 6 A :class:`~pyTooling.Versioning.VersionRange` defines a range of versions reaching from a lower to an upper bound. It equivalently supports :ref:`semantic ` and :ref:`calendar ` versions or derived subclasses thereof. When initializing a version range, an optional :class:`~pyTooling.Versioning.RangeBoundHandling` flag specifies if the bounds are inclusive (default) or exclusive. .. rubric:: Features Access bounds and bound handling behavior The lower bound of the version range can be read or updated by accessing the :attr:`~pyTooling.Versioning.VersionRange.LowerBound` property. Similarly, the upper bound of the version range can be read or updated by accessing the :attr:`~pyTooling.Versioning.VersionRange.UpperBound` property. The behavior how lower and upper bound are handled can be read or modified by accessing the :attr:`~pyTooling.Versioning.VersionRange.BoundHandling` property. Comparison of two version ranges A version range can be compare to another version range using comparison operators: ``<``, ``<=``, ``>``, ``>=``. Comparison of a version range and a version A version can be compared with a version range and vise versa using comparison operators: ``<``, ``<=``, ``>``, ``>=``. The behavior is influenced by the bound handling behavior. Contains checks A version can be checked if it's contained in a version range using *contains* operators: ``in``, ``not in``. The behavior is influenced by the bound handling behavior. Intersection Two version ranges can be intersected using the ``&`` operator creating a new version range. In case of an empty intersection result, an exception is raised. .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: python @export class VersionRange(Generic[V], metaclass=ExtendedType, slots=True): def __init__(self, lowerBound: V, upperBound: V, boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive) -> None: @property def LowerBound(self) -> V: pass @property def UpperBound(self) -> V: pass @property def BoundHandling(self) -> RangeBoundHandling: pass def __and__(self, other: Any) -> VersionRange[T]: pass def __lt__(self, other: Any) -> bool: pass def __le__(self, other: Any) -> bool: pass def __gt__(self, other: Any) -> bool: pass def __ge__(self, other: Any) -> bool: pass def __contains__(self, version: Version) -> bool: pass .. tab-set:: .. tab-item:: Instantiation (Inclusive Bounds) .. code-block:: python from pyTooling.Versioning import SemanticVersion, VersionRange versionRange = VersionRange( lowerBound=SemanticVersion(1, 0, 0), upperBound=SemanticVersion(1, 9, 0) ) testVersion = SemanticVersion(1, 4, 3) if testVersion in versionRange: pass .. tab-item:: Instantiation (Exclusive Upper Bound) .. code-block:: python from pyTooling.Versioning import SemanticVersion, VersionRange versionRange = VersionRange( lowerBound=YearWeekVersion(2023, 34), upperBound=YearWeekVersion(2023, 51), boundHandling=RangeBoundHandling.UpperBoundExclusive) ) testVersion = YearWeekVersion(2023, 51) if testVersion not in versionRange: pass .. _VERSIONING/VersionSet: VersionSet ********** .. grid:: 2 .. grid-item:: :columns: 6 A :class:`~pyTooling.Versioning.VersionSet` defines an ordered set (actually a list) of versions. It equivalently supports :ref:`semantic ` and :ref:`calendar ` versions or derived subclasses thereof. .. rubric:: Features Accessing versions in the set The versions within a version set can be accessed via index operation (``__getitem__``) or iterating (``__iter__``) the version set. The number of elements is accessible via length operation (``__len__``). Comparison of two version sets A version set can be compare to another version set using comparison operators: ``<``, ``<=``, ``>``, ``>=``. Comparison of a version set and a version A version can be compared with a version set and vise versa using comparison operators: ``<``, ``<=``, ``>``, ``>=``. Contains checks A version can be checked if it's contained in a version set using *contains* operators: ``in``, ``not in``. Intersection Two version set can be intersected using the ``&`` operator creating a new version set. In case of an empty intersection result, an exception is raised. Union Two version sets can be united using the ``|`` operator creating a new version set. .. grid-item:: :columns: 6 .. rubric:: Condensed Class Definition .. code-block:: python @export class VersionSet(Generic[V], metaclass=ExtendedType, slots=True): def __init__(self, versions: Union[Version, Iterable[V]]): pass def __and__(self, other: VersionSet[V]) -> VersionSet[T]: pass def __or__(self, other: VersionSet[V]) -> VersionSet[T]: pass def __lt__(self, other: Any) -> bool: pass def __le__(self, other: Any) -> bool: pass def __gt__(self, other: Any) -> bool: pass def __ge__(self, other: Any) -> bool: pass def __contains__(self, version: V) -> bool: pass def __len__(self) -> int: pass def __iter__(self) -> Iterator[V]: pass def __getitem__(self, index: int) -> V: pass .. tab-set:: .. tab-item:: Instantiation .. code-block:: python from pyTooling.Versioning import SemanticVersion, VersionSet versionSet = VersionSet(( YearMonthVersion(2024, 4), YearMonthVersion(2025, 1), YearMonthVersion(2019, 3) )) testVersion = YearMonthVersion(2019, 3) if testVersion in versionSet: pass .. tab-item:: Iterating Elements .. code-block:: python from pyTooling.Versioning import SemanticVersion, VersionSet versionSet = VersionSet(( YearMonthVersion(2024, 4), YearMonthVersion(2025, 1), YearMonthVersion(2019, 3) )) for version in versionSet: pass pyTooling-8.11.0/doc/Common/index.rst000066400000000000000000000110411513317154500174250ustar00rootroot00000000000000.. _COMMON: .. _COMMON/HelperFunctions: Common Helper Functions ####################### The :mod:`pyTooling.Common` package contains several useful helper functions, which are explained in the following sections. .. #contents:: Table of Contents :local: :depth: 1 .. _COMMON/Helper/getsizeof: getsizeof ********* The :func:`~pyTooling.Common.getsizeof` functions returns the "true" size of a Python object including auxiliary data structures. .. rubric:: Example: .. code-block:: Python class A: _data : int def __init__(self) -> None: _data = 5 from pyTooling.Common import getsizeof a = A() print(getsizeof(a)) .. rubric:: Details In addition to :func:`sys.getsizeof`, the used algorithm accounts also for: * ``__dict__`` * ``__slots__`` * iterable members made of: * :class:`tuple` * :class:`list` * :class:`typing.Set` * :class:`collection.deque` .. admonition:: Background Information The function :func:`sys.getsizeof` only returns the raw size of a Python object and doesn't account for the overhead of e.g. ``_dict__`` to store dynamically allocated object members. .. _COMMON/Helper/isnestedclass: isnestedclass ************* The :func:`~pyTooling.Common.isnestedclass` functions returns true, if a class is a nested class inside another class. .. rubric:: Example: .. code-block:: Python class A: class N: _data : int def __init__(self) -> None: _data = 5 N = A.N print(isnestedclass(N, A)) .. _COMMON/Helper/firstElement: firstElement ************ :func:`~pyTooling.Common.firstElement` returns the first element from an iterable. .. code-block:: Python lst = [1, 2, 3] f = firstElement(lst) # 1 .. _COMMON/Helper/lastElement: lastElement *********** :func:`~pyTooling.Common.lastElement` returns the last element from an iterable. .. code-block:: Python lst = [1, 2, 3] l = lastElement(lst) # 3 .. _COMMON/Helper/firstItem: firstItem ********* :func:`~pyTooling.Common.firstItem` returns the first item from an iterable. .. code-block:: Python lst = [1, 2, 3] f = firstItem(lst) # 1 .. _COMMON/Helper/lastItem: lastItem ******** :func:`~pyTooling.Common.lastItem` returns the last item from an iterable. .. code-block:: Python lst = [1, 2, 3] l = lastItem(lst) # 3 .. _COMMON/Helper/firstKey: firstKey ******** :func:`~pyTooling.Common.firstKey` returns the first key from a dictionary. .. code-block:: Python d = {} d["a"] = 1 d["b"] = 2 k = firstKey(d) # "a" .. hint:: The dictionary should be an order preserving dictionary, otherwise the "first" item is not defined and can return any key. .. _COMMON/Helper/firstValue: firstValue ********** :func:`~pyTooling.Common.firstValue` returns the first value from a dictionary. .. code-block:: Python d = {} d["a"] = 1 d["b"] = 2 k = firstValue(d) # 1 .. hint:: The dictionary should be an order preserving dictionary, otherwise the "first" item is not defined and can return any value. .. _COMMON/Helper/firstPair: firstPair ********* :func:`~pyTooling.Common.firstPair` returns the first pair (key-value-pair tuple) from a dictionary. .. code-block:: Python d = {} d["a"] = 1 d["b"] = 2 k = firstPair(d) # ("a", 1) .. hint:: The dictionary should be an order preserving dictionary, otherwise the "first" item is not defined and can return any pair. .. _COMMON/Helper/mergedicts: mergedicts ********** :func:`~pyTooling.Common.mergedicts` merges multiple dictionaries into a new single dictionary. It accepts an arbitrary number of dictionaries to merge. Optionally, the named parameter ``func`` accepts a function that can be applied to every element during the merge operation. .. rubric:: Example: .. code-block:: Python dictA = {11: "11", 12: "12", 13: "13"} dictB = {21: "21", 22: "22", 23: "23"} mergedDict = mergedicts(dictA, dictB) # {11: "11", 12: "12", 13: "13", 21: "21", 22: "22", 23: "23"} .. _COMMON/Helper/zipdicts: zipdicts ******** :func:`~pyTooling.Common.zipdicts` is a generator that iterates multiple dictionaries simultaneously. It expects multiple dictionary objects (fulfilling the mapping protocol) as positional parameters. An exception is raised, if not all dictionary objects have the same number of items. Also an exception is raised, if a key doesn't exist in all dictionaries. .. rubric:: Example: .. code-block:: Python dictA = {11: "11", 12: "12", 13: "13"} dictB = {11: "21", 12: "22", 13: "23"} for key, valueA, valueB in zipdicts(dictA, dictB): pass pyTooling-8.11.0/doc/Configuration/000077500000000000000000000000001513317154500171465ustar00rootroot00000000000000pyTooling-8.11.0/doc/Configuration/FileFormats.rst000066400000000000000000000036651513317154500221250ustar00rootroot00000000000000.. _CONFIG/FileFormat: File Formats ############ Currently, the following file formats are supported: * :ref:`CONFIG/FileFormat/JSON` - JavaScript Object Notation * :ref:`CONFIG/FileFormat/YAML` - YAML Ain’t Markup Language Possible future file formats: * :ref:`CONFIG/FileFormat/TOML` - Tom's Obvious, Minimal Language * :ref:`CONFIG/FileFormat/XML` - Extensible Markup Language .. tab-set:: .. tab-item:: JSON :sync: JSON .. code-block:: JSON { "version": "1.0", "settings": { "key1": "item1", "key2": "item2" }, "files": [ "path/to/file1.ext", "path/to/file2.ext", "path/to/file3.ext" ] } .. tab-item:: TOML :sync: TOML .. attention:: Not yet implemented. .. code-block:: TOML version = "1.0" [settings] key1 = "item1" key2 = "item2" files = [ "path/to/file1.ext", "path/to/file2.ext", "path/to/file3.ext" ] .. tab-item:: YAML :sync: YAML .. code-block:: YAML version: "1.0" settings: key1: item1 key2: item2 files: - path/to/file1.ext - path/to/file2.ext - path/to/file3.ext .. tab-item:: XML :sync: XML .. attention:: Not yet implemented. .. code-block:: XML item1 item2 path/to/file1.ext path/to/file2.ext path/to/file3.ext .. toctree:: :hidden: JSON TOML YAML XML pyTooling-8.11.0/doc/Configuration/JSON.rst000066400000000000000000000041641513317154500204560ustar00rootroot00000000000000.. _CONFIG/FileFormat/JSON: JSON **** Module :mod:`~pyTooling.Configuration.JSON` provides a configuration reader implementation for the JSON format. .. #contents:: Table of Contents :local: :depth: 1 .. admonition:: config.json .. code-block:: json { "version": 1 "list": [ "item_1", "item_2" ], "dict": { "key_1": "value_1", "key_2": "value_2" }, "complex": { "path": { "to": { "list": [ "item_10", "item_11", "item_12" ], "dict": { "key_10": "value_10", "key_11": "value_11" } } } } } .. seealso:: ECMA Standard 404 https://www.ecma-international.org/publications-and-standards/standards/ecma-404/ Official JSON Website https://www.json.org/json-en.html Wikipedia https://en.wikipedia.org/wiki/JSON Reading a JSON Formatted Config File ==================================== .. code-block:: python from pathlib import Path from pyTooling.Configuration.JSON import Configuration configFile = Path("config.json") config = Configuration(configFile) Accessing Values by Name ======================== .. code-block:: python # root-level scalar value configFileFormatVersion = config["version"] # value in a sequence firstItemInList = config["list"][0] # first value in dictionary firstItemInDict = config["dict"]["key_1"] Store Nodes in Variables ======================== .. code-block:: python # store intermediate node node = config["complex"]["path"]["to"] # navigate further nestedList = node["list"] nestedDict = node["dict"] Iterate Sequences ================= .. code-block:: python # simple list simpleList = config["list"] for item in simpleList: pass # deeply nested list nestedList = config["complex"]["path"]["to"]["list"] for item in nestedList: pass Iterate Dictionaries ==================== .. todo:: JSON:: Needs documentation pyTooling-8.11.0/doc/Configuration/TOML.rst000066400000000000000000000004061513317154500204530ustar00rootroot00000000000000.. _CONFIG/FileFormat/TOML: TOML **** TOML (Tom's Obvious Minimal Language) is an enhanced INI format. .. todo:: TOML:: Not yet implemented. .. seealso:: Official TOML Website https://toml.io/en/ Wikipedia https://en.wikipedia.org/wiki/TOML pyTooling-8.11.0/doc/Configuration/XML.rst000066400000000000000000000005001513317154500203330ustar00rootroot00000000000000.. _CONFIG/FileFormat/XML: XML *** XML (Extensible Markup Language). .. todo:: XML:: Not yet implemented. .. seealso:: XML 1.1 (2 :sup:`nd` Edition) https://www.w3.org/TR/2006/REC-xml-names11-20060816/ Official XML Website https://www.w3.org/XML/ Wikipedia https://en.wikipedia.org/wiki/XML pyTooling-8.11.0/doc/Configuration/YAML.rst000066400000000000000000000035571513317154500204540ustar00rootroot00000000000000.. _CONFIG/FileFormat/YAML: YAML **** Module :mod:`~pyTooling.Configuration.YAML` provides a configuration reader implementation for the YAML format. .. #contents:: Table of Contents :local: :depth: 1 .. admonition:: config.yml .. code-block:: yaml version: 1 list: - item_1 - item_2 dict: key_1: value_1 key_2: value_2 complex: path: to: list: - item_10 - item_11 - item_12 dict: key_10: value_10 key_11: value_11 .. seealso:: YAML Standard 1.2.2 https://yaml.org/spec/1.2.2/ Official YAML Website https://yaml.org/ Wikipedia https://en.wikipedia.org/wiki/YAML Reading a YAML Formatted Config File ==================================== .. code-block:: python from pathlib import Path from pyTooling.Configuration.YAML import Configuration configFile = Path("config.yml") config = Configuration(configFile) Accessing Values by Name ======================== .. code-block:: python # root-level scalar value configFileFormatVersion = config["version"] # value in a sequence firstItemInList = config["list"][0] # first value in dictionary firstItemInDict = config["dict"]["key_1"] Store Nodes in Variables ======================== .. code-block:: python # store intermediate node node = config["complex"]["path"]["to"] # navigate further nestedList = node["list"] nestedDict = node["dict"] Iterate Sequences ================= .. code-block:: python # simple list simpleList = config["list"] for item in simpleList: pass # deeply nested list nestedList = config["complex"]["path"]["to"]["list"] for item in nestedList: pass Iterate Dictionaries ==================== .. todo:: YAML:: Needs documentation pyTooling-8.11.0/doc/Configuration/index.rst000066400000000000000000000162751513317154500210220ustar00rootroot00000000000000.. _CONFIG: Configuration ############# Module :mod:`~pyTooling.Configuration` provides an abstract configuration reader. .. #contents:: Table of Contents :local: :depth: 1 It supports any configuration file syntax, which provides: * scalar elements (integer, string, ...), * sequences (ordered lists), and * dictionaries (key-value-pairs). The abstracted data model is based on a common :class:`~pyTooling.Configuration.Node` class, which is derived to a :class:`~pyTooling.Configuration.Sequence`, :class:`~pyTooling.Configuration.Dictionary` and :class:`~pyTooling.Configuration.Configuration` class. .. rubric:: Inheritance diagram: .. inheritance-diagram:: pyTooling.Configuration :parts: 1 Dictionary ********** A :class:`~pyTooling.Configuration.Dictionary` represents key-value-pairs of information. .. tab-set:: .. tab-item:: JSON :sync: JSON .. code-block:: JSON // one-liner style {"key1": "item1", "key2": "item2", "key3": "item3"} // multi-line style { "key1": "item1", "key2": "item2", "key3": "item3" } .. tab-item:: TOML :sync: TOML .. code-block:: TOML # one-liner style section_1 = {key1 = "item1", key2 = "item2", key3 = "item3"} # section style [section_2] key1 = "item1" key2 = "item2" key3 = "item3" .. tab-item:: YAML :sync: YAML .. code-block:: YAML # one-liner style {key1: item1, key2: item2, key3: item3} # multi-line style key1: item1 key2: item2 key3: item3 .. tab-item:: XML :sync: XML .. code-block:: XML item1 item2 item3 .. todo:: CONFIG:: Needs documentation for Dictionary Sequences ********* A :class:`~pyTooling.Configuration.Sequence` represents ordered information items. .. tab-set:: .. tab-item:: JSON :sync: JSON .. code-block:: JSON // one-liner style ["item1", "item2", "item3"] // multi-line style [ "item1", "item2", "item3" ] .. tab-item:: TOML :sync: TOML .. code-block:: TOML # one-liner style section_1 = ["item1", "item2", "item3"] # multi-line style section_2 = [ "item1", "item2", "item3" ] .. tab-item:: YAML :sync: YAML .. code-block:: YAML # one-liner style [item1, item2, item3] # multi-line style - item1 - item2 - item3 .. tab-item:: XML :sync: XML .. code-block:: XML item1 item2 item3 .. todo:: CONFIG:: Needs documentation for Sequences Configuration ************* A :class:`~pyTooling.Configuration.Configuration` represents the whole configuration (file) made of sequences, dictionaries and scalar information items. .. tab-set:: .. tab-item:: JSON :sync: JSON .. code-block:: JSON { "version": "1.0", "settings": { "key1": "item1", "key2": "item2" }, "files": [ "path/to/file1.ext", "path/to/file2.ext", "path/to/file3.ext" ] } .. tab-item:: TOML :sync: TOML .. attention:: Not yet implemented. .. code-block:: TOML version = "1.0" [settings] key1 = "item1" key2 = "item2" files = [ "path/to/file1.ext", "path/to/file2.ext", "path/to/file3.ext" ] .. tab-item:: YAML :sync: YAML .. code-block:: YAML version: "1.0" settings: key1: item1 key2: item2 files: - path/to/file1.ext - path/to/file2.ext - path/to/file3.ext .. tab-item:: XML :sync: XML .. attention:: Not yet implemented. .. code-block:: XML item1 item2 path/to/file1.ext path/to/file2.ext path/to/file3.ext .. todo:: CONFIG:: Needs documentation for Configuration Data Model ********** .. todo:: CONFIG:: Needs documentation for Data Model .. mermaid:: flowchart TD Configuration --> Dictionary Configuration --> Sequence Dictionary --> Dictionary Sequence --> Sequence Dictionary --> Sequence Sequence --> Dictionary Creating a Concrete Implementation ********************************** Follow these steps to derive a concrete implementation of the abstract configuration data model. 1. Import classes from abstract data model .. code-block:: python from . import ( Node as Abstract_Node, Dictionary as Abstract_Dict, Sequence as Abstract_Seq, Configuration as Abstract_Configuration, KeyT, NodeT, ValueT ) 2. Derive a node, which might hold references to nodes in the source file's parser for later usage. .. code-block:: python @export class Node(Abstract_Node): _configNode: Union[CommentedMap, CommentedSeq] # further local fields def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, configNode: Union[CommentedMap, CommentedSeq]) -> None: Abstract_Node.__init__(self, root, parent) self._configNode = configNode # Implement mandatory methods and properties 3. Derive a dictionary class: .. code-block:: python @export class Dictionary(Node, Abstract_Dict): def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, configNode: CommentedMap) -> None: Node.__init__(self, root, parent, key, configNode) # Implement mandatory methods and properties 4. Derive a sequence class: .. code-block:: python @export class Sequence(Node, Abstract_Seq): def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, configNode: CommentedSeq) -> None: Node.__init__(self, root, parent, key, configNode) # Implement mandatory methods and properties 5. Set new dictionary and sequence classes as types in the abstract node class. .. code-block:: python setattr(Abstract_Node, "DICT_TYPE", Dictionary) setattr(Abstract_Node, "SEQ_TYPE", Sequence) 6. Derive a configuration class: .. code-block:: python @export class Configuration(Dictionary, Abstract_Configuration): def __init__(self, configFile: Path) -> None: with configFile.open() as file: self._config = ... Dictionary.__init__(self, self, self, None, self._config) # Implement mandatory methods and properties pyTooling-8.11.0/doc/DataStructures/000077500000000000000000000000001513317154500173145ustar00rootroot00000000000000pyTooling-8.11.0/doc/DataStructures/Cartesian.rst000066400000000000000000000043251513317154500217630ustar00rootroot00000000000000.. _STRUCT/Cartesian2D: 2D Cartesian ############ The :mod:`pyTooling.Cartesian2D` package ... .. #contents:: Table of Contents :local: :depth: 2 .. rubric:: 2D Cartesian Properties: * Coordinates as ``int`` or ``float``. * TBD .. _STRUCT/Cartesian2D/Features: Features ******** * TBD .. _STRUCT/Cartesian2D/MissingFeatures: Missing Features ================ * TBD .. _STRUCT/Cartesian2D/PlannedFeatures: Planned Features ================ * TBD .. _STRUCT/Cartesian2D/RejectedFeatures: Out of Scope ============ * TBD .. _STRUCT/Cartesian2D/ByFeature: By Feature ********** .. danger:: Accessing internal fields of a origin, point, offset, ... is strongly not recommended for users, as it might lead to a corrupted data structure. If a power-user wants to access these fields, feel free to use them for achieving a higher performance, but you got warned 😉. .. _STRUCT/Cartesian2D/Classes: Basic Classes ************* .. _STRUCT/Cartesian2D/Origin2D: Origin2D ======== .. _STRUCT/Cartesian2D/Point2D: Point2D ======= .. _STRUCT/Cartesian2D/Offset2D: Offset2D ======== .. _STRUCT/Cartesian2D/Size2D: Size2D ====== .. _STRUCT/Cartesian2D/Segment2D: Segment2D ========= .. _STRUCT/Cartesian2D/LineSegment2D: LineSegment2D ============= .. _STRUCT/Cartesian2D/Shapes: Shapes ****** .. _STRUCT/Cartesian2D/Shape: Shape ===== .. _STRUCT/Cartesian2D/Trapezium: Trapezium ========= .. _STRUCT/Cartesian2D/Rectangle: Rectangle ========= .. _STRUCT/Cartesian2D/Square: Square ====== .. _STRUCT/Cartesian3D: 3D Cartesian ############ The :mod:`pyTooling.Cartesian3D` package ... .. _STRUCT/Cartesian3D/Classes: Basic Classes ************* .. _STRUCT/Cartesian3D/Origin3D: Origin3D ======== .. _STRUCT/Cartesian3D/Point3D: Point3D ======= .. _STRUCT/Cartesian3D/Offset3D: Offset3D ======== .. _STRUCT/Cartesian3D/Size3D: Size3D ====== .. _STRUCT/Cartesian3D/Segment3D: Segment3D ========= .. _STRUCT/Cartesian3D/LineSegment3D: LineSegment3D ============= .. _STRUCT/Cartesian3D/Volumes: Volumes ******* .. _STRUCT/Cartesian3D/Volume: Volume ====== .. _STRUCT/Cartesian3D/Cuboid: Cuboid ====== .. _STRUCT/Cartesian3D/Cube: Cube ==== pyTooling-8.11.0/doc/DataStructures/Graph.rst000066400000000000000000000151211513317154500211070ustar00rootroot00000000000000.. _STRUCT/Graph: Graph ##### The :mod:`pyTooling.Graph` package provides a directed graph data structure. Compared to :ref:`NetworkX ` and :ref:`igraph `, this implementation provides an object-oriented API. .. #contents:: Table of Contents :local: :depth: 2 .. rubric:: Example Graph: .. mermaid:: :caption: A directed graph with backward-edges denoted by dotted vertex relations. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph LR A(A); B(B); C(C); D(D); E(E); F(F) ; G(G); H(H); I(I) A --> B --> E G --> F A --> C --> G --> H --> D D -.-> A D & F -.-> B I ---> E --> F --> D classDef node fill:#eee,stroke:#777,font-size:smaller; .. rubric:: Graph Properties: A **graph** data structure is represented by an instance of :class:`~pyTooling.Graph.Graph` holding references to all nodes. Nodes are instances of :class:`~pyTooling.Graph.Vertex` classes and directed links between nodes are made of :class:`~pyTooling.Graph.Edge` instances. A graph can have attached meta information as key-value pairs. Graph algorithms using all vertexes are provided as methods on the graph instance. Whereas graph algorithms based on a starting vertex are provided as methods on a vertex. A **vertex** can have a unique ID, a value and attached meta information as key-value pairs. A vertex has references to inbound and outbound edges, thus a graph can be traversed in reverse. An **edge** can have a unique ID, a value, a weight and attached meta information as key-value pairs. All edges are directed. .. note:: The data structure reaches similar performance as :gh:`NetworkX `, while the API follows object-oriented-programming principles instead of procedural programming principles. The following example code demonstrates a few features in a compact form: .. code-block:: python # Create a new graph graph = Graph(name="Example Graph") .. _STRUCT/Graph/Features: Features ******** * Fast and powerful graph data structure. * Operations on vertexes following directed edges. * Operations on whole graph. * A vertex and an edge can have a unique ID. * A vertex and an edge can have a value. * A graph, vertex and an edge can store key-value-pairs via dictionary syntax. * A vertex knows its inbound and outbound edges. * An edge can have a weight. .. _STRUCT/Graph/MissingFeatures: Missing Features ================ * TBD .. _STRUCT/Graph/PlannedFeatures: Planned Features ================ * TBD .. _STRUCT/Graph/RejectedFeatures: Out of Scope ============ * Preserve or recover the graph data structure before an erroneous operation caused an exception and aborted a graph modification, which might leave the graph in a corrupted state. * Export the graph data structure to various file formats like JSON, YAML, TOML, ... * Import a graph data structure from various file formats like JSON, YAML, TOML, ... * Graph visualization or rendering to complex formats like GraphML, GraphViz, Mermaid, ... .. _STRUCT/Graph/ByFeature: By Feature ********** .. danger:: Accessing internal fields of a graph, vertex or edge is strongly not recommended for users, as it might lead to a corrupted graph data structure. If a power-user wants to access these fields, feel free to use them for achieving a higher performance, but you got warned 😉. .. _STRUCT/Graph/ID: Unique ID ========= A vertex can be created with a unique ID when the object is created. Afterwards, the :attr:`~pyTooling.Graph.Vertex.ID` is a readonly property. Any hashable object can be used as an ID. The ID must be unique per graph. If graphs are merged or vertexes are added to an existing graph, the newly added graph's ID(s) are checked and might cause an exception. Also edges can be created with a unique ID when the object is created. Afterwards, the :attr:`~pyTooling.Graph.Edge.ID` is a readonly property. Any hashable object can be used as an ID. The ID must be unique per graph. If graphs are merged or vertexes are added to an existing graph, the newly added graph's ID(s) are checked and might cause an exception. .. code-block:: python # Create vertex with unique ID 5 graph = Graph() vertex = Vertex(vertexID=5, graph=graph) # Read a vertex's ID vertexID = vertex.ID .. _STRUCT/Graph/Value: Value ===== A vertex's value can be given at vertex creating time or it can be set ant any later time via property :attr:`~pyTooling.Graph.Vertex.Value`. Any data type is accepted. The internally stored value can be retrieved by the same property. If a vertex's string representation is requested via :meth:`~pyTooling.Graph.Vertex.__str__` and a vertex's value isn't None, then the value's string representation is returned. .. todo:: GRAPH: setting / getting an edge's values .. code-block:: python # Create vertex with unique ID 5 graph = Graph() vertex = Vertex(value=5, graph=graph) # Set or change a node's value vertex.Value = 10 # Access a vertex's Value value = vertex.Value .. _STRUCT/Graph/KeyValuePairs: Key-Value-Pairs =============== .. todo:: GRAPH: setting / getting a vertex's KVPs .. todo:: GRAPH: setting / getting an edge's KVPs .. _STRUCT/Graph/Inbound: Inbound Edges ============= .. todo:: GRAPH: inbound edges .. _STRUCT/Graph/Outbound: Outbound Edges ============== .. todo:: GRAPH: outbound edges .. _STRUCT/Graph/GraphRef: Graph Reference =============== .. todo:: GRAPH: reference to the graph .. _STRUCT/Graph/Competitors: Competing Solutions ******************* Compared to :gh:`NetworkX ` and :gh:`igraph `, this implementation provides an object-oriented API. .. _STRUCT/Graph/NetworkX: NetworkX ======== .. rubric:: Disadvantages * Many operations are executed on the graph, but not on vertex/node objects or edge objects. * Algorithms are provided as functions instead of methods. * Vertices are created implicitly. * ... .. rubric:: Standoff * Arbitrary data can be attached to edges. * ... .. rubric:: Advantages * A huge variety of algorithms is provided. * ... .. code-block:: python import networkx as nx G = nx.Graph() G.add_edge("A", "B", weight=4) G.add_edge("B", "D", weight=2) G.add_edge("A", "C", weight=3) G.add_edge("C", "D", weight=4) nx.shortest_path(G, "A", "D", weight="weight") .. _STRUCT/Graph/igraph: igraph ====== .. todo:: GRAPH::igraph write example and demonstrate missing OOP API. .. rubric:: Disadvantages * ... .. rubric:: Standoff * ... .. rubric:: Advantages * ... .. code-block:: python # add code here pyTooling-8.11.0/doc/DataStructures/LinkedList.rst000066400000000000000000000331421513317154500221130ustar00rootroot00000000000000.. _STRUCT/LinkedList: Doubly Linked List ################## The :mod:`pyTooling.LinkedList` package ... .. #contents:: Table of Contents :local: :depth: 2 .. rubric:: Example Doubly Linked List: .. mermaid:: :caption: A linked list graph. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph LR LL(LinkedList); A(Node 0); B(Node 1); C(Node 2); D(Node 3); E(Node 4) LL:::mark1 --> A LL --> E A <--> B <--> C <--> D <--> E classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; .. rubric:: Doubly Linked List Properties: * The LinkedList counts the number of elements. * The LinkedList has a reference to the first and last element in the doubly linked list. * Each Node has a linked to its previous Node and its next Node (doubly linked). * Each Node has a link to its LinkedList. * Each Node has a value. * Operations can be performed in the LinkedList or on any Node. * The LinkedList can be iterated in ascending and descending order. * The list can be iterated starting from any Node. .. _STRUCT/LinkedList/Features: Features ******** * Insert operations can be performed in the LinkedList or on any Node. * Remove operations can be performed in the LinkedList or on any Node. * The LinkedList can be iterated in ascending and descending order. * The LinkedList can be cleared. * TBD .. _STRUCT/LinkedList/MissingFeatures: Missing Features ================ * TBD .. _STRUCT/LinkedList/PlannedFeatures: Planned Features ================ * TBD .. _STRUCT/LinkedList/RejectedFeatures: Out of Scope ============ * TBD .. _STRUCT/LinkedList/ByProperty: By Property *********** Linked List Properties ====================== * is empty * count * first * last Node Properties =============== * PreviousNode * NextNode * Value * List .. _STRUCT/LinkedList/ByOperation: By Operation ************ .. danger:: Accessing internal fields of a doubly linked list or node is strongly not recommended for users, as it might lead to a corrupted data structure. If a power-user wants to access these fields, feel free to use them for achieving a higher performance, but you got warned 😉. .. _STRUCT/LinkedList/Instantiation: Instantiation ============= .. grid:: 2 .. grid-item:: :columns: 6 The :class:`~pyTooling.LinkedList.LinkedList` can be instantiated as an empty linked list without any aditional parameters. It will report as empty via property :attr:`LinkedList.IsEmpty ` and report zero elements via property :attr:`LinkedList.Count `. Alternatively, it can be constructed from an iterable like a :class:`tuple`, :class:`list` or any Python iterator. The order of the iterable is preserved. The time complexity is `O(n)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Empty LinkedList .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() ll.IsEmpty # => True ll.Count # => 0 .. tab-item:: LinkedList from tuple .. code-block:: Python from pyTooling.LinkedList import LinkedList initTuple = (1, 2, 3, 4, 5) ll = LinkedList(initTuple) ll.IsEmpty # => False ll.Count # => 5 Clear ===== .. grid:: 2 .. grid-item:: :columns: 6 The :class:`~pyTooling.LinkedList.LinkedList` can be cleared by calling the :meth:`LinkedList.Clear ` method. Afterwards, the linked list reports as empty and a count of zero. The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Clearing a LinkedList .. code-block:: Python from pyTooling.LinkedList import LinkedList initTuple = (1, 2, 3, 4, 5) ll = LinkedList(initTuple) ll.Clear() ll.IsEmpty # => False ll.Count # => 5 Insert ====== .. grid:: 2 .. grid-item:: :columns: 6 A new :class:`~pyTooling.LinkedList.Node` can be inserted into the linked list at any position. Very fast insertions into the linked list can be achieved before the the first element using :meth:`LinkedList.InsertBeforeFirst ` or after the last element using :meth:`LinkedList.InsertAfterLast ` Additionally, if there is a reference to a specific node of the linked list, insertions before and after that node are also very efficient. The methods are :meth:`LinkedList.InsertNodeBefore ` and :meth:`LinkedList.InsertNodeAfter `. The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Before first node .. code-block:: Python from pyTooling.LinkedList import LinkedList, Node initTuple = (1, 2, 3, 4, 5) ll = LinkedList(initTuple) newNode = Node(0) ll.InsertBeforeFirst(newNode) .. tab-item:: After last node .. code-block:: Python from pyTooling.LinkedList import LinkedList, Node initTuple = (1, 2, 3, 4, 5) ll = LinkedList(initTuple) newNode = Node(6) ll.InsertAfterLast(newNode) .. tab-item:: Before current node .. code-block:: Python from pyTooling.LinkedList import LinkedList, Node initTuple = (1, 2, 3, 4, 5) ll = LinkedList(initTuple) node = ll[2] newNode = Node(2.5) node.InsertNodeBefore(newNode) .. tab-item:: After current node .. code-block:: Python from pyTooling.LinkedList import LinkedList, Node initTuple = (1, 2, 3, 4, 5) ll = LinkedList(initTuple) node = ll[2] newNode = Node(3.5) node.InsertNodeAfter(newNode) Random Access Insert ==================== .. grid:: 2 .. grid-item:: :columns: 6 Inserting a new node at a random postion is less efficient then direct inserts at the first or last element of the linked list or before and after a specific node. The additional effort comes from walking the linked list to find the n-th element. Then an efficient insert is performed. The linked list is walked from the shorter end. The time complexity is `O(n/2)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Before first n-th node .. code-block:: Python from pyTooling.LinkedList import LinkedList, Node initTuple = (1, 2, 3, 4, 5) ll = LinkedList(initTuple) newNode = Node(2.5) ll.InsertNodeBefore(2, newNode) .. tab-item:: Before after n-th node .. code-block:: Python from pyTooling.LinkedList import LinkedList, Node initTuple = (1, 2, 3, 4, 5) ll = LinkedList(initTuple) newNode = Node(3.5) ll.InsertNodeAfter(2, newNode) Remove ====== .. grid:: 2 .. grid-item:: :columns: 6 A new :class:`~pyTooling.LinkedList.Node` can be inserted into the linked list at any position. Very fast insertions can be achieved before the the first element using :meth:`LinkedList.InsertBeforeFirst ` The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: First node .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: Last node .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: Current node .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: At position .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: By predicate .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() Iterate ======= .. grid:: 2 .. grid-item:: :columns: 6 A new :class:`~pyTooling.LinkedList.Node` can be inserted into the linked list at any position. Very fast insertions can be achieved before the the first element using :meth:`LinkedList.InsertBeforeFirst ` The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Forward from first node .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: Backward from last node .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: Forward from current node .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: Backward from current node .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() Sort ==== .. grid:: 2 .. grid-item:: :columns: 6 A new :class:`~pyTooling.LinkedList.Node` can be inserted into the linked list at any position. Very fast insertions can be achieved before the the first element using :meth:`LinkedList.InsertBeforeFirst ` The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Ascending .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: Descending .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() Reverse ======= .. grid:: 2 .. grid-item:: :columns: 6 A new :class:`~pyTooling.LinkedList.Node` can be inserted into the linked list at any position. Very fast insertions can be achieved before the the first element using :meth:`LinkedList.InsertBeforeFirst ` The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Reverse .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() Search ====== .. grid:: 2 .. grid-item:: :columns: 6 A new :class:`~pyTooling.LinkedList.Node` can be inserted into the linked list at any position. Very fast insertions can be achieved before the the first element using :meth:`LinkedList.InsertBeforeFirst ` The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: By predicate .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() Convert ======= .. grid:: 2 .. grid-item:: :columns: 6 A new :class:`~pyTooling.LinkedList.Node` can be inserted into the linked list at any position. Very fast insertions can be achieved before the the first element using :meth:`LinkedList.InsertBeforeFirst ` The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: To tuple .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: To list .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() Item Access =========== .. grid:: 2 .. grid-item:: :columns: 6 A new :class:`~pyTooling.LinkedList.Node` can be inserted into the linked list at any position. Very fast insertions can be achieved before the the first element using :meth:`LinkedList.InsertBeforeFirst ` The time complexity is `O(1)`. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Get value .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: Set value .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() .. tab-item:: Delete value .. code-block:: Python from pyTooling.LinkedList import LinkedList ll = LinkedList() pyTooling-8.11.0/doc/DataStructures/Path/000077500000000000000000000000001513317154500202105ustar00rootroot00000000000000pyTooling-8.11.0/doc/DataStructures/Path/URL.rst000066400000000000000000000024311513317154500214040ustar00rootroot00000000000000.. _STRUCT/Path/URL: Unified Resource Locator (URL) ############################## Implements a *Unified Resource Locator* (URL). .. code-block:: Python url = URL.Parse("http://paebbels:xxx@semaphore.plc2.de:5000/api/v1/semaphore?name=Riviera&foo=bar#page2") print(url.Scheme()) # HTTP print(url.User()) # paebbels print(url.Password()) # xxx print(url.Host()) # semaphore.plc2.de:5000 print(url.Path()) # /api/v1/semaphore print(url.Query()) # name=Riviera&foo=bar print(url.Fragment()) # page2 .. _STRUCT/Path/URL/Protocols: Protocols ********* .. #autoclass:: pyTooling.GenericPath.URL.Protocols :show-inheritance: :members: :private-members: .. _STRUCT/Path/URL/Host: Host **** .. #autoclass:: pyTooling.GenericPath.URL.Host :show-inheritance: :members: :private-members: .. _STRUCT/Path/URL/Element: Element ******* .. #autoclass:: pyTooling.GenericPath.URL.Element :show-inheritance: :members: :private-members: .. _STRUCT/Path/URL/Path: Path **** .. #autoclass:: pyTooling.GenericPath.URL.Path :show-inheritance: :members: :private-members: .. _STRUCT/Path/URL/URL: URL *** .. #autoclass:: pyTooling.GenericPath.URL.URL :show-inheritance: :members: :private-members: pyTooling-8.11.0/doc/DataStructures/Path/index.rst000066400000000000000000000023521513317154500220530ustar00rootroot00000000000000.. _STRUCT/Path: Path #### .. _STRUCT/Path/Generic: GenericPath *********** .. _STRUCT/Path/Generic/Base: Base ==== .. todo:: GenericPath:: Needs documentation for Base .. #autoclass:: pyTooling.GenericPath.Base :show-inheritance: :members: :private-members: .. _STRUCT/Path/Generic/RootMixIn: RootMixIn ========= .. todo:: GenericPath:: Needs documentation for RootMixIn .. #autoclass:: pyTooling.GenericPath.RootMixIn :show-inheritance: :members: :private-members: .. _STRUCT/Path/Generic/ElementMixIn: ElementMixIn ============ .. todo:: GenericPath:: Needs documentation for ElementMixIn .. #autoclass:: pyTooling.GenericPath.ElementMixIn :show-inheritance: :members: :private-members: .. _STRUCT/Path/Generic/SystemMixIn: SystemMixIn =========== .. todo:: GenericPath:: Needs documentation for SystemMixIn .. #autoclass:: pyTooling.GenericPath.SystemMixIn :show-inheritance: :members: :private-members: .. _STRUCT/Path/Generic/PathMixIn: PathMixIn ========= .. todo:: GenericPath:: Needs documentation for PathMixIn .. #autoclass:: pyTooling.GenericPath.PathMixIn :show-inheritance: :members: :private-members: Specific Implementations ************************ .. toctree:: URL pyTooling-8.11.0/doc/DataStructures/StateMachine.rst000066400000000000000000000030031513317154500224070ustar00rootroot00000000000000.. _STRUCT/StateMachine: StateMachine ############ The :mod:`pyTooling.StateMachine` package .. #contents:: Table of Contents :local: :depth: 2 .. rubric:: Example Statemachine: .. mermaid:: :caption: A statemachine graph. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD A(Idle); B(Check); C(Prepare); D(Read); E(Finished); F(Write) ; G(Retry); H(WriteWait); I(ReadWait) A:::mark1 --> B --> C --> F F --> H --> E:::cur B --> G --> B G -.-> A --> C D -.-> A C ---> D --> I --> E -.-> A classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; .. rubric:: Statemachine Properties: .. _STRUCT/StateMachine/Features: Features ******** * TBD .. _STRUCT/StateMachine/MissingFeatures: Missing Features ================ * TBD .. _STRUCT/StateMachine/PlannedFeatures: Planned Features ================ * TBD .. _STRUCT/StateMachine/RejectedFeatures: Out of Scope ============ * TBD .. _STRUCT/StateMachine/ByFeature: By Feature ********** .. danger:: Accessing internal fields of a statemachine, state or transition is strongly not recommended for users, as it might lead to a corrupted data structure. If a power-user wants to access these fields, feel free to use them for achieving a higher performance, but you got warned 😉. .. _STRUCT/StateMachine/ID: Unique ID ========= pyTooling-8.11.0/doc/DataStructures/Tree.rst000066400000000000000000001766501513317154500207640ustar00rootroot00000000000000.. _STRUCT/Tree: Tree #### The :mod:`pyTooling.Tree` package provides fast and simple tree data structure based on a single :class:`~pyTooling.Tree.Node` class. .. hint:: This tree data structure outperforms :gh:`anytree ` by far and even :gh:`itertree ` by factor of 2. Further alternatives: **treelib** :gh:`treelib ` .. #contents:: Table of Contents :local: :depth: 3 .. rubric:: Example Tree: .. mermaid:: :caption: Root of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R:::mark1 --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; .. rubric:: Comprehensive Example: The following example code demonstrates a few features in a compact form: .. code-block:: python # Create a new tree by creating a root node (no parent reference) root = Node(value="OSVVM Regression Tests") # Construct the tree top-down lib = Node(value="Utility Library", parent=root) # Another standalone node with unique ID (actually an independent tree) common = Node(nodeID=5, value="Common") # Construct bottom-up axi = Node(value="AXI") axiCommon = Node(value="AXI4 Common") axi.AddChild(axiCommon) # Group nodes and handover children at node creation time vcList = [common, axi] vcs = Node(value="Verification Components", parent=root, children=vcList) # Add multiple nodes at once axiProtocols = ( Node(value="AXI4-Stream"), Node(value="AXI4-Lite"), Node(value="AXI4") ) axi.AddChildren(axiProtocols) # Create another standalone node and attach it later to a tree. uart = Node(value="UART") uart.Parent = vcs The presented code will generate this tree: .. code-block:: OSVVM Regression Tests ├── Utility Library ├── Verification Components ├── Common ├── AXI │ ├── AXI4 Common │ ├── AXI4-Stream │ ├── AXI4-Lite │ ├── AXI4 ├── UART .. _STRUCT/Tree/Features: Features ******** * Fast and simple tree data structure based on a single :class:`~pyTooling.Tree.Node` class. * A tree can be constructed top-down and bottom-up. * A node can have a unique ID. * A node knows its level (distance from root). * A node can have a value. * A node can store key-value-pairs via dictionary syntax. * A node has a reference to its parent node. * A node has a reference to the root node in a tree (representative node). * Rendering to simple ASCII art for debugging purposes. .. _STRUCT/Tree/MissingFeatures: Missing Features ================ * Insert a node (currently, only add/append is supported). * Move a node in same hierarchy level. * Move node to a different level/node in the same tree in a single operation. * Allow node deletion. .. _STRUCT/Tree/PlannedFeatures: Planned Features ================ * Allow filters (predicates) in generators to allow node filtering. * Tree export to formats like GraphML, ... * Export the tree data structure to file the YAML format. * Allow nodes to have tags and group nodes by tags. * Allow nodes to link to other nodes (implement proxy behavior?) .. _STRUCT/Tree/RejectedFeatures: Out of Scope ============ * Preserve or recover the tree data structure before an erroneous operation caused an exception and aborted a tree modification, which might leave the tree in a corrupted state. * Export the tree data structure to various file formats like JSON, TOML, ... * Import a tree data structure from various file formats like JSON, YAML, TOML, ... * Tree visualization or rendering to complex formats like GraphViz, Mermaid, ... .. _STRUCT/Tree/ByFeature: By Feature ********** .. danger:: Accessing internal fields of a node is strongly not recommended for users, as it might lead to a corrupted tree data structure. If a power-user wants to access these fields, feel free to use them for achieving a higher performance, but you got warned 😉. .. _STRUCT/Tree/ID: Unique ID ========= A node can be created with a unique ID when the object is created. Afterwards, the :attr:`~pyTooling.Tree.Node.ID` is a readonly property. Any hashable object can be used as an ID. The ID must be unique per tree. If trees are merged or nodes are added to an existing tree, the newly added node's ID(s) are checked and might cause an exception. .. code-block:: python # Create node with unique ID 5 node = Node(nodeID=5) # Read a node's ID nodeID = node.ID .. _STRUCT/Tree/Level: Level ===== Each node has a level describing the distance from :term:`root node `. It can be accessed via the read-only property :attr:`~pyTooling.Tree.Node.Level`. The root node has a level of ``0``, children of root have a level of ``1``, and so on. .. code-block:: python # Create node root = Node(nodeID=0) node2 = Node(nodeID=1, parent=root) # Read a node's level nodeLevel = node2.Level .. _STRUCT/Tree/Value: Value ===== A node's value can be given at node creating time or it can be set ant any later time via property :attr:`~pyTooling.Tree.Node.Value`. Any data type is accepted. The internally stored value can be retrieved by the same property. If a node's string representation is requested via :meth:`~pyTooling.Tree.Node.__str__` and a node's value isn't None, then the value's string representation is returned. .. code-block:: python # Create node with value 5 node = Node(value=5) # Set or change a node's value node.Value = 10 # Access a node's Value value = node.Value .. _STRUCT/Tree/KeyValuePairs: Key-Value-Pairs =============== Besides a :ref:`unique ID ` and a :ref:`value `, each node can hold an arbitrary set of key-value-pairs. .. code-block:: python # Create node node = Node() # Create or update a key-value-pair node["key"] = value # Access a value by key value = node["key"] # Delete a key-value-pair del node["key"] .. _STRUCT/Tree/Parent: Parent Reference ================ Each node has a reference to its :term:`parent node `. In case, the node is the :term:`root node `, the parent reference is :data:`None`. The parent-child relation can be set at node creation time, or a parent can be assigned to a node at any later time via property :attr:`~pyTooling.Tree.Node.Parent`. The same property can be used to retrieve the current parent reference. .. code-block:: python # Create node without parent relation ship (root node) root = Node(nodeID=0) # Create a node add directly attach it to an existing tree node = Node(nodeID=1, parent=root) # Access a node's parent parent = node.Parent Merging Trees ------------- In case, two trees were created (a single node is already a minimal tree), trees get merged if one tree's root node is assigned a parent relationship. .. code-block:: python # Create a tree with a single node root = Node(nodeID=0) # Create a second minimalistic tree otherTree = Node(nodeID=100) # Set parent relationship and merge trees otherTree.Parent = root .. seealso:: See :ref:`STRUCT/Tree/Merging` for more details. Splitting Trees --------------- In case, a node within a tree's hierarchy is updated with respect to it's parent relationship to :data:`None`, then the tree gets split into 2 trees. .. code-block:: python # Create a tree of 4 nodes root1 = Node(nodeID=0) node1 = Node(nodeID=1, parent=root1) root2 = Node(nodeID=2, parent=node1) node3 = Node(nodeID=3, parent=root2) # Split the tree between node1 and root2 root2.Parent = None .. seealso:: See :ref:`STRUCT/Tree/Splitting` for more details. Moving a branch in same tree ---------------------------- .. todo:: TREE::Parent::move-branch in same tree - needs also testcases Moving a branch to another tree ------------------------------- .. todo:: TREE::Parent::move-branch into another tree - needs also testcases .. _STRUCT/Tree/Root: Root Reference ============== Each node has a reference to the tree's :term:`root node `. The root node can also be considered the representative node of a tree and can be accessed via read-only property :attr:`~pyTooling.Tree.Node.Root`. When a node is assigned a new parent relation and this parent is a node in another tree, the root reference will change. (A.k.a. moving a branch to another tree.) The root node of a tree contains tree-wide data structures like the list of unique IDs (:attr:`~pyTooling.Tree.Node._nodesWithID`, :attr:`~pyTooling.Tree.Node._nodesWithoutID`). By utilizing the root reference, each node can access these data structures by just one additional reference hop. .. code-block:: python # Create a simple tree root = Node() nodeA = Node(parent=root) nodeB = Node(parent=root) # Check if nodeA and nodeB are in same tree isSameTree = nodeA is nodeB .. _STRUCT/Tree/Path: Path ==== The property :attr:`~pyTooling.Tree.Node.Path` returns a tuple describing the path top-down from root node to the current node. .. code-block:: python # Create a simple tree representing directories root = Node(value="C:") dir = Node(value="temp", parent=root) file = Node(value="test.log", parent=dir) # Convert a path to string path = "\".join(file.Path) While the tuple returned by :attr:`~pyTooling.Tree.Node.Path` can be used in an iteration (e.g. a for-loop), also a generator is provided by method :meth:`~pyTooling.Tree.Node.GetPath` for iterations. .. code-block:: python # Create a simple tree representing directories root = Node(value="C:") dir = Node(value="temp", parent=root) file = Node(value="test.log", parent=dir) # Render path from root to node with indentations to ASCII art for level, node in enumerate(file.GetPath()): print(f"{' '*level}'\-'{node}") # \-C: # \-temp # \-test.log .. _STRUCT/Tree/Ancestors: Ancestors ========= The method :meth:`~pyTooling.Tree.Node.GetAncestors` returns a generator to traverse bottom-up from current node to the root node. If the top-down direction is needed, see :ref:`STRUCT/Tree/Path` for more details. +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | Python Code | Diagram | +=====================================================+=====================================================================================================================+ | .. rubric:: Tree Construction: | .. mermaid:: | | .. code-block:: python | | | | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | | # Create an example tree | graph TD | | root = Node(nodeID=0) | R(Root) | | dots = Node(nodeID=1, parent=root) | A(...) | | node1 = Node(nodeID=2, parent=dots) | BL(Node); B(GrandParent); BR(Node) | | grandParent = Node(nodeID=3, parent=dots) | CL(Uncle); C(Parent); CR(Aunt) | | node2 = Node(nodeID=4, parent=dots) | DL(Sibling); D(Node); DR(Sibling) | | uncle = Node(nodeID=5, parent=grandParent) | ELN1(Niece); ELN2(Nephew) | | parent = Node(nodeID=6, parent=grandParent) | EL(Child); E(Child); ER(Child); | | aunt = Node(nodeID=7, parent=grandParent) | ERN1(Niece);ERN2(Nephew) | | sibling1 = Node(nodeID=8, parent=parent) | F1(GrandChild); F2(GrandChild) | | me = Node(nodeID=9, parent=parent) | | | sibling2 = Node(nodeID=10, parent=parent) | R:::mark1 --> A | | niece1 = Node(nodeID=11, parent=sibling1) | A:::mark2 --> BL & B & BR | | nephew1 = Node(nodeID=12, parent=sibling1) | B:::mark2 --> CL & C & CR | | child1 = Node(nodeID=13, parent=me) | C:::mark2 --> DL & D & DR | | child2 = Node(nodeID=14, parent=me) | DL --> ELN1 & ELN2 | | child3 = Node(nodeID=15, parent=me) | D:::cur --> EL & E & ER | | niece2 = Node(nodeID=16, parent=sibling2) | DR --> ERN1 & ERN2 | | nephew2 = Node(nodeID=17, parent=sibling2) | E --> F1 & F2 | | grandChild1 = Node(nodeID=18, parent=child2) | | | grandChild2 = Node(nodeID=19, parent=child2) | classDef node fill:#eee,stroke:#777,font-size:smaller; | | | classDef cur fill:#9e9,stroke:#6e6; | | .. rubric:: Usage | classDef mark1 fill:#69f,stroke:#37f,color:#eee; | | .. code-block:: python | classDef mark2 fill:#69f,stroke:#37f; | | | | | # Walk bottom-up all the way to root | | | for node in me.GetAncestors(): | | | print(node.ID) | | | | | | .. rubric:: Result | | | .. code-block:: | | | | | | 6 # parent | | | 3 # grandparent | | | 1 # ... | | | 0 # root | | +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ .. _STRUCT/Tree/CommonAncestors: Common Ancestors ---------------- If needed, method :meth:`~pyTooling.Tree.Node.GetCommonAncestors` provides a generator to iterate the common ancestors of two nodes in a tree. It iterates from root node top-down until the common branch in the tree splits of. +---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | Python Code | Diagram | +=========================================================+=====================================================================================================================+ | .. rubric:: Tree Construction: | .. mermaid:: | | .. code-block:: python | | | | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | | # Create an example tree | graph TD | | root = Node(nodeID=0) | R(Root) | | dots = Node(nodeID=1, parent=root) | A(...) | | node1 = Node(nodeID=2, parent=dots) | BL(Node); B(GrandParent); BR(Node) | | grandParent = Node(nodeID=3, parent=dots) | CL(Uncle); C(Parent); CR(Aunt) | | node2 = Node(nodeID=4, parent=dots) | DL(Sibling); D(Node); DR(Sibling) | | uncle = Node(nodeID=5, parent=grandParent) | ELN1(Niece); ELN2(Nephew) | | parent = Node(nodeID=6, parent=grandParent) | EL(Child); E(Child); ER(Child); | | aunt = Node(nodeID=7, parent=grandParent) | ERN1(Niece);ERN2(Nephew) | | sibling1 = Node(nodeID=8, parent=parent) | F1(GrandChild); F2(GrandChild) | | me = Node(nodeID=9, parent=parent) | | | sibling2 = Node(nodeID=10, parent=parent) | R:::mark1 --> A | | niece1 = Node(nodeID=11, parent=sibling1) | A:::mark2 --> BL & B & BR | | nephew1 = Node(nodeID=12, parent=sibling1) | B:::mark2 --> CL & C & CR | | child1 = Node(nodeID=13, parent=me) | C:::mark2 --> DL & D & DR | | child2 = Node(nodeID=14, parent=me) | DL --> ELN1 & ELN2 | | child3 = Node(nodeID=15, parent=me) | D --> EL & E & ER | | niece2 = Node(nodeID=16, parent=sibling2) | DR --> ERN1 & ERN2 | | nephew2 = Node(nodeID=17, parent=sibling2) | E --> F1 & F2 | | grandChild1 = Node(nodeID=18, parent=child2) | ELN2:::cur; F2:::cur | | grandChild2 = Node(nodeID=19, parent=child2) | classDef node fill:#eee,stroke:#777,font-size:smaller; | | | classDef cur fill:#9e9,stroke:#6e6; | | .. rubric:: Usage | classDef mark1 fill:#69f,stroke:#37f,color:#eee; | | .. code-block:: python | classDef mark2 fill:#69f,stroke:#37f; | | | | | # Walk bottom-up all the way to root | | | for node in nephew1.GetCommonAncestors(grandChild2): | | | print(node.ID) | | | | | | .. rubric:: Result | | | .. code-block:: | | | | | | 0 # root | | | 1 # ... | | | 3 # grandparent | | | 6 # parent | | +---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ .. _STRUCT/Tree/Children: Children ======== :term:`Children ` are all direct successors of a :term:`node`. A node object supports returning children either as a tuple via a property or as a generator via a method call. +-------------------------------+-----------------------------------------------+--------------------------------------------------+ | | Return a Tuple | Return a Generator | +===============================+===============================================+==================================================+ | Children | :attr:`~pyTooling.Tree.Node.Children` | :meth:`~pyTooling.Tree.Node.GetChildren` | +-------------------------------+-----------------------------------------------+--------------------------------------------------+ | Children and children thereof | — — — — | :meth:`~pyTooling.Tree.Node.GetDescendants` | +-------------------------------+-----------------------------------------------+--------------------------------------------------+ +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | Python Code | Diagram | +=====================================================+=====================================================================================================================+ | .. rubric:: Tree Construction: | .. mermaid:: | | .. code-block:: python | | | | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | | # Create an example tree | graph TD | | root = Node(nodeID=0) | R(Root) | | dots = Node(nodeID=1, parent=root) | A(...) | | node1 = Node(nodeID=2, parent=dots) | BL(Node); B(GrandParent); BR(Node) | | grandParent = Node(nodeID=3, parent=dots) | CL(Uncle); C(Parent); CR(Aunt) | | node2 = Node(nodeID=4, parent=dots) | DL(Sibling); D(Node); DR(Sibling) | | uncle = Node(nodeID=5, parent=grandParent) | ELN1(Niece); ELN2(Nephew) | | parent = Node(nodeID=6, parent=grandParent) | EL(Child); E(Child); ER(Child); | | aunt = Node(nodeID=7, parent=grandParent) | ERN1(Niece);ERN2(Nephew) | | sibling1 = Node(nodeID=8, parent=parent) | F1(GrandChild); F2(GrandChild) | | me = Node(nodeID=9, parent=parent) | | | sibling2 = Node(nodeID=10, parent=parent) | R --> A | | niece1 = Node(nodeID=11, parent=sibling1) | A --> BL & B & BR | | nephew1 = Node(nodeID=12, parent=sibling1) | B --> CL & C & CR | | child1 = Node(nodeID=13, parent=me) | C --> DL & D & DR | | child2 = Node(nodeID=14, parent=me) | DL --> ELN1 & ELN2 | | child3 = Node(nodeID=15, parent=me) | D:::cur --> EL & E & ER | | niece2 = Node(nodeID=16, parent=sibling2) | DR --> ERN1 & ERN2 | | nephew2 = Node(nodeID=17, parent=sibling2) | E --> F1 & F2 | | grandChild1 = Node(nodeID=18, parent=child2) | EL:::mark2; E:::mark2; ER:::mark2 | | grandChild2 = Node(nodeID=19, parent=child2) | classDef node fill:#eee,stroke:#777,font-size:smaller; | | | classDef cur fill:#9e9,stroke:#6e6; | | .. rubric:: Usage | classDef mark1 fill:#69f,stroke:#37f,color:#eee; | | .. code-block:: python | classDef mark2 fill:#69f,stroke:#37f; | | | | | # Walk bottom-up all the way to root | | | for node in me.GetChildren(): | | | print(node.ID) | | | | | | .. rubric:: Result | | | .. code-block:: | | | | | | 13 # child1 | | | 14 # child2 | | | 15 # child3 | | +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ .. _STRUCT/Tree/Descendants: Descendants =========== :term:`Descendants ` are all direct and indirect successors of a :term:`node` (:term:`child nodes ` and child nodes thereof a.k.a. :term:`grandchild`, grand-grandchildren, ...). A node object supports returning descendants as a generator via a method call to :meth:`~pyTooling.Tree.Node.GetDescendants`, due to the recursive behavior. .. seealso:: See :ref:`STRUCT/Tree/Iterating` for various other forms for iterating nodes in a tree. +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | Python Code | Diagram | +=====================================================+=====================================================================================================================+ | .. rubric:: Tree Construction: | .. mermaid:: | | .. code-block:: python | | | | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | | # Create an example tree | graph TD | | root = Node(nodeID=0) | R(Root) | | dots = Node(nodeID=1, parent=root) | A(...) | | node1 = Node(nodeID=2, parent=dots) | BL(Node); B(GrandParent); BR(Node) | | grandParent = Node(nodeID=3, parent=dots) | CL(Uncle); C(Parent); CR(Aunt) | | node2 = Node(nodeID=4, parent=dots) | DL(Sibling); D(Node); DR(Sibling) | | uncle = Node(nodeID=5, parent=grandParent) | ELN1(Niece); ELN2(Nephew) | | parent = Node(nodeID=6, parent=grandParent) | EL(Child); E(Child); ER(Child); | | aunt = Node(nodeID=7, parent=grandParent) | ERN1(Niece);ERN2(Nephew) | | sibling1 = Node(nodeID=8, parent=parent) | F1(GrandChild); F2(GrandChild) | | me = Node(nodeID=9, parent=parent) | | | sibling2 = Node(nodeID=10, parent=parent) | R --> A | | niece1 = Node(nodeID=11, parent=sibling1) | A --> BL & B & BR | | nephew1 = Node(nodeID=12, parent=sibling1) | B --> CL & C & CR | | child1 = Node(nodeID=13, parent=me) | C --> DL & D & DR | | child2 = Node(nodeID=14, parent=me) | DL --> ELN1 & ELN2 | | child3 = Node(nodeID=15, parent=me) | D:::cur --> EL & E & ER | | niece2 = Node(nodeID=16, parent=sibling2) | DR --> ERN1 & ERN2 | | nephew2 = Node(nodeID=17, parent=sibling2) | E --> F1 & F2 | | grandChild1 = Node(nodeID=18, parent=child2) | EL:::mark2; E:::mark2; ER:::mark2; F1:::mark2; F2:::mark2 | | grandChild2 = Node(nodeID=19, parent=child2) | classDef node fill:#eee,stroke:#777,font-size:smaller; | | | classDef cur fill:#9e9,stroke:#6e6; | | .. rubric:: Usage | classDef mark1 fill:#69f,stroke:#37f,color:#eee; | | .. code-block:: python | classDef mark2 fill:#69f,stroke:#37f; | | | | | # Walk bottom-up all the way to root | | | for node in me.GetDescendants(): | | | print(node.ID) | | | | | | .. rubric:: Result | | | .. code-block:: | | | | | | 13 # child1 | | | 14 # child2 | | | 18 # grandChild1 | | | 19 # grandChild2 | | | 15 # child3 | | +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ .. _STRUCT/Tree/Siblings: Siblings ======== :term:`Siblings ` are all direct :term:`child nodes ` of a node's :term:`parent` node except itself. A node object supports returning siblings either as tuples via a property or as a generator via a method call. Either all siblings are returned or just siblings left from the current node (left siblings) or right from the current node (right siblings). Left and right is based on the order of child references in the current node's parent. +-------------------+-----------------------------------------------+--------------------------------------------------+ | Sibling Selection | Return a Tuple | Return a Generator | +===================+===============================================+==================================================+ | Left Siblings | :attr:`~pyTooling.Tree.Node.LeftSiblings` | :meth:`~pyTooling.Tree.Node.GetLeftSiblings` | +-------------------+-----------------------------------------------+--------------------------------------------------+ | All Siblings | :attr:`~pyTooling.Tree.Node.Siblings` | :meth:`~pyTooling.Tree.Node.GetSiblings` | +-------------------+-----------------------------------------------+--------------------------------------------------+ | Right Siblings | :attr:`~pyTooling.Tree.Node.RightSiblings` | :meth:`~pyTooling.Tree.Node.GetRightSiblings` | +-------------------+-----------------------------------------------+--------------------------------------------------+ .. attention:: In case a node has no parent, an exception is raised, because siblings cannot exist. +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | Python Code | Diagram | +=====================================================+=====================================================================================================================+ | .. rubric:: Tree Construction: | .. mermaid:: | | .. code-block:: python | | | | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | | # Create an example tree | graph TD | | root = Node(nodeID=0) | R(Root) | | dots = Node(nodeID=1, parent=root) | A(...) | | node1 = Node(nodeID=2, parent=dots) | BL(Node); B(GrandParent); BR(Node) | | grandParent = Node(nodeID=3, parent=dots) | CL(Uncle); C(Parent); CR(Aunt) | | node2 = Node(nodeID=4, parent=dots) | DL(Sibling); D(Node); DR(Sibling) | | uncle = Node(nodeID=5, parent=grandParent) | ELN1(Niece); ELN2(Nephew) | | parent = Node(nodeID=6, parent=grandParent) | EL(Child); E(Child); ER(Child); | | aunt = Node(nodeID=7, parent=grandParent) | ERN1(Niece);ERN2(Nephew) | | sibling1 = Node(nodeID=8, parent=parent) | F1(GrandChild); F2(GrandChild) | | me = Node(nodeID=9, parent=parent) | | | sibling2 = Node(nodeID=10, parent=parent) | R --> A | | niece1 = Node(nodeID=11, parent=sibling1) | A --> BL & B & BR | | nephew1 = Node(nodeID=12, parent=sibling1) | B --> CL & C & CR | | child1 = Node(nodeID=13, parent=me) | C --> DL & D & DR | | child2 = Node(nodeID=14, parent=me) | DL --> ELN1 & ELN2 | | child3 = Node(nodeID=15, parent=me) | D:::cur --> EL & E & ER | | niece2 = Node(nodeID=16, parent=sibling2) | DR --> ERN1 & ERN2 | | nephew2 = Node(nodeID=17, parent=sibling2) | E --> F1 & F2 | | grandChild1 = Node(nodeID=18, parent=child2) | DL:::mark2; DR:::mark2 | | grandChild2 = Node(nodeID=19, parent=child2) | classDef node fill:#eee,stroke:#777,font-size:smaller; | | | classDef cur fill:#9e9,stroke:#6e6; | | .. rubric:: Usage | classDef mark1 fill:#69f,stroke:#37f,color:#eee; | | .. code-block:: python | classDef mark2 fill:#69f,stroke:#37f; | | | | | # Walk bottom-up all the way to root | | | for node in me.GetLeftSiblings(): | | | print(node.ID) | | | for node in me.GetRightSiblings(): | | | print(node.ID) | | | | | | .. rubric:: Result | | | .. code-block:: | | | | | | 8 # sibling1 | | | 10 # sibling2 | | +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ .. _STRUCT/Tree/Relatives: Relatives ========= :term:`Relatives ` are :term:`siblings ` and their :term:`descendants `. A node object supports returning relatives as a generator via a method call, due to the recursive behavior. Either all relatives are returned or just relatives left from the current node (left relatives) or right from the current node (right relatives). Left and right is based on the order of child references in the current node's parent. +--------------------+---------------------------------------------------+ | Relative Selection | Return a Generator | +====================+===================================================+ | Left Siblings | :meth:`~pyTooling.Tree.Node.GetLeftRelatives` | +--------------------+---------------------------------------------------+ | All Siblings | :meth:`~pyTooling.Tree.Node.GetRelatives` | +--------------------+---------------------------------------------------+ | Right Siblings | :meth:`~pyTooling.Tree.Node.GetRightRelatives` | +--------------------+---------------------------------------------------+ .. attention:: In case a node has no parent, an exception is raised, because siblings and therefore relatives cannot exist. +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | Python Code | Diagram | +=====================================================+=====================================================================================================================+ | .. rubric:: Tree Construction: | .. mermaid:: | | .. code-block:: python | | | | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | | # Create an example tree | graph TD | | root = Node(nodeID=0) | R(Root) | | dots = Node(nodeID=1, parent=root) | A(...) | | node1 = Node(nodeID=2, parent=dots) | BL(Node); B(GrandParent); BR(Node) | | grandParent = Node(nodeID=3, parent=dots) | CL(Uncle); C(Parent); CR(Aunt) | | node2 = Node(nodeID=4, parent=dots) | DL(Sibling); D(Node); DR(Sibling) | | uncle = Node(nodeID=5, parent=grandParent) | ELN1(Niece); ELN2(Nephew) | | parent = Node(nodeID=6, parent=grandParent) | EL(Child); E(Child); ER(Child); | | aunt = Node(nodeID=7, parent=grandParent) | ERN1(Niece);ERN2(Nephew) | | sibling1 = Node(nodeID=8, parent=parent) | F1(GrandChild); F2(GrandChild) | | me = Node(nodeID=9, parent=parent) | | | sibling2 = Node(nodeID=10, parent=parent) | R --> A | | niece1 = Node(nodeID=11, parent=sibling1) | A --> BL & B & BR | | nephew1 = Node(nodeID=12, parent=sibling1) | B --> CL & C & CR | | child1 = Node(nodeID=13, parent=me) | C --> DL & D & DR | | child2 = Node(nodeID=14, parent=me) | DL --> ELN1 & ELN2 | | child3 = Node(nodeID=15, parent=me) | D:::cur --> EL & E & ER | | niece2 = Node(nodeID=16, parent=sibling2) | DR --> ERN1 & ERN2 | | nephew2 = Node(nodeID=17, parent=sibling2) | E --> F1 & F2 | | grandChild1 = Node(nodeID=18, parent=child2) | DL:::mark2; ELN1:::mark2; ELN2:::mark2; DR:::mark2; ERN1:::mark2; ERN2:::mark2 | | grandChild2 = Node(nodeID=19, parent=child2) | classDef node fill:#eee,stroke:#777,font-size:smaller; | | | classDef cur fill:#9e9,stroke:#6e6; | | .. rubric:: Usage | classDef mark1 fill:#69f,stroke:#37f,color:#eee; | | .. code-block:: python | classDef mark2 fill:#69f,stroke:#37f; | | | | | # Walk bottom-up all the way to root | | | for node in me.GetLeftRelatives(): | | | print(node.ID) | | | for node in me.GetRightRelatives(): | | | print(node.ID) | | | | | | .. rubric:: Result | | | .. code-block:: | | | | | | 8 # sibling1 | | | 11 # niece1 | | | 12 # nephew1 | | | | | | 10 # sibling2 | | | 16 # niece2 | | | 17 # nephew2 | | +-----------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ .. _STRUCT/Tree/Iterating: Iterating a Tree ================ A tree (starting at the :term:`root node `) or a subtree (starting at any node in the tree) can be iterated in various orders: * :meth:`~pyTooling.Tree.Node.IterateLeafs` - iterates only over leafs from left to right * :meth:`~pyTooling.Tree.Node.IterateLevelOrder` - iterates all sub nodes level by level * :meth:`~pyTooling.Tree.Node.IteratePreOrder` - iterates left to right and returns itself before its descendants * :meth:`~pyTooling.Tree.Node.IteratePostOrder` - iterates left to right and returns its descendants before itself .. _STRUCT/Tree/Merging: Merging Trees ============= A tree **B** is merged into an existing tree **A**, when a tree **B**'s parent relation is set to a non-:data:`None` value. Therefore use the :attr:`B.Parent ` property and set it to **A**: :pycode:`B.Parent = A`. The following operations are executed on the tree **B**: 1. register all nodes of **B** with and without ID in **A**, then 2. delete the list and dictionary objects for nodes with and without IDs from **B**. The following operations are executed on all nodes in tree **B**: * set root reference to **A**. * recompute the level within **A**. .. attention:: In case a node's ID already exists in **A**, an exception is raised, because IDs are unique. .. _STRUCT/Tree/Splitting: Splitting Trees =============== .. todo:: TREE: splitting a tree .. _STRUCT/Tree/Rendering: Tree Rendering ============== The tree data structure can be rendered as ASCII art. The :meth:`~pyTooling.Tree.Node.Render` method renders the tree into a multi line string. .. todo:: TREE:Render:: explain parameters .. admonition:: Example .. code-block:: o-- | o-- | | o-- | | o-- | o-- | o-- | o-- | o-- | o-- o-- o-- o-- o-- .. _STRUCT/Tree/Competitors: Competing Solutions ******************* This tree data structure outperforms :gh:`anytree ` by far and even :gh:`itertree ` by factor of 2. .. _STRUCT/Tree/anytree: anytree ======= Source: :gh:`anytree ` .. todo:: TREE::anytree write comparison here. .. rubric:: Disadvantages * ... .. rubric:: Standoff * ... .. rubric:: Advantages * ... .. code-block:: python # add code here .. _STRUCT/Tree/itertree: itertree ======== Source: :gh:`itertree ` .. todo:: TREE::itertree write comparison here. .. rubric:: Disadvantages * ... .. rubric:: Standoff * ... .. rubric:: Advantages * ... .. code-block:: python # add code here .. _STRUCT/Tree/treelib: treelib ======= Source: :gh:`treelib ` .. todo:: TREE::treelib write comparison here. .. rubric:: Disadvantages * ... .. rubric:: Standoff * ... .. rubric:: Advantages * ... .. code-block:: python # add code here pyTooling-8.11.0/doc/DataStructures/index.rst000066400000000000000000000642271513317154500211700ustar00rootroot00000000000000.. _STRUCT: Overview ######## Currently, the following data structures are implemented: * :ref:`STRUCT/Path/Generic` * :ref:`STRUCT/Path/URL` +---------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | :ref:`STRUCT/Graph` | :ref:`STRUCT/Tree` | :ref:`STRUCT/Statemachine` | +=====================================================================================================================+====================================================================================================================+=====================================================================================================================+ | .. mermaid:: | .. mermaid:: | .. mermaid:: | | :caption: A directed graph with backward-edges denoted by dotted vertex relations. | :caption: Root of the current node are marked in blue. | :caption: A statemachine graph. | | | | | | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% | | graph LR | graph TD | graph TD | | A(A); B(B); C(C); D(D); E(E); F(F) ; G(G); H(H); I(I) | R(Root) | A(Idle); B(Check); C(Prepare); D(Read); E(Finished); F(Write) ; G(Retry); H(WriteWait); I(ReadWait) | | | A(...) | | | A --> B --> E | BL(Node); B(GrandParent); BR(Node) | A:::mark1 --> B --> C --> F | | G --> F | CL(Uncle); C(Parent); CR(Aunt) | F --> H --> E:::cur | | A --> C --> G --> H --> D | DL(Sibling); D(Node); DR(Sibling) | B --> G --> B | | D -.-> A | ELN1(Niece); ELN2(Nephew) | G -.-> A --> C | | D & F -.-> B | EL(Child); E(Child); ER(Child); | D -.-> A | | I ---> E --> F --> D | ERN1(Niece);ERN2(Nephew) | C ---> D --> I --> E -.-> A | | | F1(GrandChild); F2(GrandChild) | | | classDef node fill:#eee,stroke:#777,font-size:smaller; | | classDef node fill:#eee,stroke:#777,font-size:smaller; | | classDef node fill:#eee,stroke:#777,font-size:smaller; | R:::mark1 --> A | classDef cur fill:#9e9,stroke:#6e6; | | classDef node fill:#eee,stroke:#777,font-size:smaller; | A --> BL & B & BR | classDef mark1 fill:#69f,stroke:#37f,color:#eee; | | | B --> CL & C & CR | | | | C --> DL & D & DR | | | | DL --> ELN1 & ELN2 | | | | D:::cur --> EL & E & ER | | | | DR --> ERN1 & ERN2 | | | | E --> F1 & F2 | | | | | | | | classDef node fill:#eee,stroke:#777,font-size:smaller; | | | | classDef cur fill:#9e9,stroke:#6e6; | | | | classDef mark1 fill:#69f,stroke:#37f,color:#eee; | | | | | | +---------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | .. code-block:: python | .. code-block:: python | .. code-block:: python | | | | | | from pyTooling.Graph import Graph | from pyTooling.Tree import Node | from pyTooling.StateMachine import FSM, State | | | | | | graph = Graph(name="Example Graph") | root = Node(id="Root") | | | | | | | # Create a standalone vertex A in the graph | dir1 = Node(id="Dir1", parent=root) | | | rootA = Vertex(value="A", graph=graph) | dir2 = Node(id="Dir2", parent=root) | | | | file0 = Node(id="File0", parent=root) | | | # Add 2 vertices B,C and add edges from A | dir3 = Node(id="Dir3", parent=root) | | | edgeAB = rootA.EdgeToNewVertex(vertexValue="B") | | | | edgeAC = rootA.EdgeToNewVertex(vertexValue="C") | file1 = Node(id="File1", parent=dir1) | | | | file2 = Node(id="File2", parent=dir1) | | | # Get vertices B,C | file3 = Node(id="File3", parent=dir1) | | | vertexB = edgeAB.Destination | | | | vertexC = edgeAC.Destination | file4 = Node(id="File4", parent=dir2) | | | | | | | # Add more standalone vertices D,E,F,G | file5 = Node(id="File5", parent=dir3) | | | vertexD = Vertex(value="D", graph=graph) | file6 = Node(id="File6", parent=dir3) | | | vertexE = Vertex(value="E", graph=graph) | | | | vertexF = Vertex(value="F", graph=graph) | | | | vertexG = Vertex(value="G", graph=graph) | | | | | | | | # Create edges between B-E,C-G,D-A,D-B | | | | vertexB.EdgeTo(vertexE) | | | | vertexC.EdgeTo(vertexG) | | | | vertexD.EdgeTo(rootA) | | | | vertexD.EdgeTo(vertexB) | | | | vertexE.EdgeTo(vertexF) | | | | vertexF.EdgeTo(vertexB) | | | | vertexG.EdgeTo(vertexF) | | | | | | | | # Create edge from | | | | vertexD.EdgeFrom(vertexF) | | | | | | | | # Add vertex I,H and add edge from new vertex to existing | | | | vertexE.EdgeFromNewVertex(vertexValue="I") | | | | vertexD.EdgeFromNewVertex(vertexValue="H") | | | | | | | | # Lookup vertices and link them | | | | vertexG.EdgeTo(graph.GetVertexByValue("H")) | | | | | | | +---------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ pyTooling-8.11.0/doc/Decorators.rst000066400000000000000000000300221513317154500171730ustar00rootroot00000000000000.. _DECO: Overview ######## The :mod:`pyTooling.Decorators` package provides decorators to: * mark functions or methods as *not implemented*. * control the visibility of classes and functions defined in a module. * help with copying doc-strings from base-classes. .. #contents:: Table of Contents :depth: 2 .. _DECO/Abstract: Abstract Methods ################ .. grid:: 2 .. grid-item:: :columns: 6 .. todo:: DECO:: Refer to :func:`~pyTooling.MetaClasses.abstractmethod` and :func:`~pyTooling.MetaClasses.mustoverride` decorators from :ref:`meta classes `. .. important:: Classes using method decorators :ref:`@abstractmethod ` or :ref:`@mustoverride ` need to use the meta-class :ref:`ExtendedType `. Alternatively, classes can be derived from :ref:`SlottedObject ` or apply decorators :ref:`DECO/slotted` or :ref:`DECO/mixin`. .. _DECO/AbstractMethod: @abstractmethod *************** .. grid:: 2 .. grid-item:: :columns: 6 The :func:`~pyTooling.MetaClasses.abstractmethod` decorator marks a method as *abstract*. The original method gets replaced by a method raising a :exc:`NotImplementedError`. This can happen, if an abstract method is overridden but called via :pycode:`super()...`. When a class containing *abstract* methods is instantiated, an :exc:`~pyTooling.Exceptions.AbstractClassError` is raised. .. hint:: If the abstract method contains code that should be called from an overriding method in a derived class, use the :ref:`@mustoverride ` decorator. .. important:: The class declaration must apply the metaclass :ref:`ExtendedType ` so the decorator has an effect. .. grid-item:: :columns: 6 .. code-block:: Python class A(metaclass=ExtendedType): @abstractmethod def method(self) -> int: """Methods documentation.""" class B(A): @InheritDocString(A) def method(self) -> int: return 2 .. _DECO/MustOverride: @mustoverride ************* .. grid:: 2 .. grid-item:: :columns: 6 The :func:`~pyTooling.MetaClasses.mustoverride` decorator marks a method as *must override*. When a class containing *must override* methods is instantiated, an :exc:`~pyTooling.Exceptions.MustOverrideClassError` is raised. In contrast to :ref:`@abstractmethod `, the method can still be called from a derived class implementing an overridden method. .. hint:: If the method contain no code and if it should throw an exception when called, use the :ref:`@abstractmethod ` decorator. .. important:: The class declaration must apply the metaclass :ref:`ExtendedType ` so the decorator has an effect. .. grid-item:: :columns: 6 .. code-block:: Python class A(metaclass=ExtendedType): @mustoverride def method(self) -> int: """Methods documentation.""" return 2 class B(A): @InheritDocString(A) def method(self) -> int: result = super().method() return result + 1 .. _DECO/DataAccess: Data Access ########### .. _DECO/readonly: @readonly ********* .. grid:: 2 .. grid-item:: :columns: 6 The :func:`~pyTooling.Decorators.readonly` decorator makes a property *read-only*. Thus the properties :pycode:`setter` and :pycode:`deleter` can't be used. .. grid-item:: :columns: 6 .. code-block:: Python class Data: _data: int def __init__(self, data: int) -> None: self._data = data @readonly def Length(self) -> int: return 2 ** self._data .. _DECO/classproperty: @classproperty ************** .. grid:: 2 .. grid-item:: :columns: 6 .. attention:: Class properties are currently broken in Python. .. _DECO/Documentation: Documentation ############# .. _DECO/export: @export ******* .. grid:: 2 .. grid-item:: :columns: 6 The :func:`~pyTooling.Decorators.export` decorator makes module's entities (classes and functions) publicly visible. Therefore, these entities get registered in the module's variable ``__all__``. Besides making these entities accessible via ``from foo import *``, Sphinx extensions like autoapi are reading ``__all__`` to infer what entities from a module should be auto documented. .. grid-item:: :columns: 6 .. code-block:: Python # Creating __all__ is only required, if variables need to be listed too __all__ = ["MY_CONST"] # Decorators can't be applied to fields, so it was manually registered in __all__ MY_CONST = 42 @export class MyClass: """This is a public class.""" @export def myFunc(): """This is a public function.""" # Each application of "@export" will append an entry to __all__ .. #admonition:: ``application.py`` .. code-block:: python from .module import * inst = MyClass() .. _DECO/InheritDocString: @InheritDocString ***************** .. grid:: 2 .. grid-item:: :columns: 6 When a method in a derived class shall have the same doc-string as the doc-string of the base-class, then the decorator :func:`~pyTooling.Decorators.InheritDocString` can be used to copy the doc-string from base-class' method to the method in the derived class. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Inherit class documentation .. code-block:: Python class BaseClass: """Method's doc-string.""" @InheritDocString(BaseClass, merge=True) class DerivedClass(BaseClass): """Will ne written underneath""" .. tab-item:: Inherit method documentation .. code-block:: Python class BaseClass: def method(self): """Method's doc-string.""" class DerivedClass(BaseClass): @InheritDocString(BaseClass) def method(self): pass .. _DECO/Performance: Performance ########### .. _DECO/slotted: @slotted ******** .. grid:: 2 .. grid-item:: :columns: 6 The size of class instances (objects) can be reduced by using :ref:`slots`. This decreases the object creation time and memory footprint. In addition access to fields faster because there is no time consuming field lookup in ``__dict__``. A class with 2 ``__dict__`` members has around 520 B whereas the same class structure uses only around 120 B if slots are used. On CPython 3.10 using slots, the code accessing class fields is 10..25 % faster. The :class:`~pyTooling.MetaClasses.ExtendedType` meta-class can automatically infer slots from type annotations. Because the syntax for applying a meta-class is quite heavy, this decorator simplifies the syntax. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Syntax using Decorator ``slotted`` .. code-block:: Python @export @slotted class A: _field1: int _field2: str def __init__(self, arg1: int, arg2: str) -> None: self._field1 = arg1 self._field2 = arg2 .. tab-item:: Syntax using meta-class ``ExtendedType`` .. code-block:: Python @export class A(metaclass=ExtendedType, slots=True): _field1: int _field2: str def __init__(self, arg1: int, arg2: str) -> None: self._field1 = arg1 self._field2 = arg2 .. _DECO/mixin: @mixin ****** .. grid:: 2 .. grid-item:: :columns: 6 The size of class instances (objects) can be reduced by using :ref:`slots` (see :ref:`DECO/slotted`). If slots are used in multiple inheritance scenarios, only one ancestor line can use slots. For other ancestor lines, it's allowed to define the slot fields in the inheriting class. Therefore pyTooling allows marking classes as :term:`mixin-classes `. The :class:`~pyTooling.MetaClasses.ExtendedType` meta-class can automatically infer slots from type annotations. If a class is marked as a mixin-class, the inferred slots are collected and handed over to class defining slots. Because the syntax for applying a meta-class is quite heavy, this decorator simplifies the syntax. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Syntax using Decorator ``mixin`` .. code-block:: Python @export @slotted class A: _field1: int _field2: str def __init__(self, arg1: int, arg2: str) -> None: self._field1 = arg1 self._field2 = arg2 @export class B(A): _field3: int _field4: str def __init__(self, arg1: int, arg2: str) -> None: self._field3 = arg1 self._field4 = arg2 super().__init__(arg1, arg2) @export @mixin class C(A): _field5: int _field6: str def Method(self) -> str: return f"{self._field5} -> {self._field6}" @export class D(B, C): def __init__(self, arg1: int, arg2: str) -> None: super().__init__(arg1, arg2) .. tab-item:: Syntax using meta-class ``ExtendedType`` .. code-block:: Python @export class A(metaclass=ExtendedType, slots=True): _field1: int _field2: str def __init__(self, arg1: int, arg2: str) -> None: self._field1 = arg1 self._field2 = arg2 @export class B(A): _field3: int _field4: str def __init__(self, arg1: int, arg2: str) -> None: self._field3 = arg1 self._field4 = arg2 super().__init__(arg1, arg2) @export class C(A, mixin=True): _field5: int _field6: str def Method(self) -> str: return f"{self._field5} -> {self._field6}" @export class D(B, C): def __init__(self, arg1: int, arg2: str) -> None: super().__init__(arg1, arg2) .. _DECO/singleton: @singleton ********** .. grid:: 2 .. grid-item:: :columns: 6 .. todo:: DECO::singleton needs documentation .. _DECO/Misc: Miscellaneous ############# .. _DECO/notimplemented: @notimplemented *************** .. grid:: 2 .. grid-item:: :columns: 6 The :func:`~pyTooling.Decorators.notimplemented` decorator replaces a callable (function or method) with a callable raising a :exc:`NotImplementedError` containing the decorators message parameter as an error message. The original callable might contain code, but it's made unreachable by the decorator. The callable's name and doc-string is copied to the replacing callable. A reference to the original callable is preserved in the :pycode:`.__orig_func__` field. .. grid-item:: :columns: 6 .. code-block:: Python class Data: @notimplemented("This function isn't tested yet.") def method(self, param: int): return 2 ** param pyTooling-8.11.0/doc/Dependency.rst000066400000000000000000000755241513317154500171640ustar00rootroot00000000000000.. _DEP: Dependencies ############ .. |img-pyTooling-lib-status| image:: https://img.shields.io/librariesio/release/pypi/pyTooling :alt: Libraries.io status for latest release :height: 22 :target: https://libraries.io/github/pyTooling/pyTooling .. |img-pyTooling-vul-status| image:: https://img.shields.io/snyk/vulnerabilities/github/pyTooling/pyTooling :alt: Snyk Vulnerabilities for GitHub Repo :height: 22 :target: https://img.shields.io/snyk/vulnerabilities/github/pyTooling/pyTooling +------------------------------------------+------------------------------------------+ | `Libraries.io `_ | Vulnerabilities Summary | +==========================================+==========================================+ | |img-pyTooling-lib-status| | |img-pyTooling-vul-status| | +------------------------------------------+------------------------------------------+ .. _DEP/package: pyTooling Package (Mandatory) ***************************** .. rubric:: Manually Installing Package Requirements Use the :file:`requirements.txt` file to install all dependencies via ``pip3`` or install the package directly from PyPI (see :ref:`INSTALL`). .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash pip3 install -U -r requirements.txt .. tab-item:: Windows :sync: Windows .. code-block:: powershell pip install -U -r requirements.txt .. rubric:: Dependency List When installed as ``pyTooling``: +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Package** | **Version** | **License** | **Dependencies** | +=================================================================+=============+===========================================================================================+========================================================================================================================================================+ | *No dependencies* | — — — — | — — — — | — — — — | +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ When installed as ``pyTooling[packaging]``: +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Package** | **Version** | **License** | **Dependencies** | +=================================================================+=============+===========================================================================================+========================================================================================================================================================+ | `setuptools `__ | ≥80.0 | `BSD-3-Clause `__ | None | +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ .. todo:: investigate dependencies and licenses of setuptools. When installed as ``pyTooling[terminal]``: +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Package** | **Version** | **License** | **Dependencies** | +=================================================================+=============+===========================================================================================+========================================================================================================================================================+ | `colorama `__ | ≥0.4.6 | `BSD-3-Clause `__ | None | +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ When installed as ``pyTooling[yaml]``: +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Package** | **Version** | **License** | **Dependencies** | +=================================================================+=============+===========================================================================================+========================================================================================================================================================+ | `ruamel.yaml `__ | ≥0.18 | `MIT `__ | *Not yet evaluated.* | +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ .. todo:: investigate dependencies and licenses of ruamel.yaml. .. _DEP/testing: Unit Testing (Optional) *********************** Unit Testing / Coverage / Type Checking (Optional) ================================================== Additional Python packages needed for testing, code coverage collection and static type checking. These packages are only needed for developers or on a CI server, thus sub-dependencies are not evaluated further. .. rubric:: Manually Installing Test Requirements Use the :file:`tests/requirements.txt` file to install all dependencies via ``pip3``. The file will recursively install the mandatory dependencies too. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash pip install -U -r tests/requirements.txt .. tab-item:: Windows :sync: Windows .. code-block:: powershell pip3 install -U -r tests\requirements.txt .. rubric:: Dependency List - Unit Testing +---------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | **Package** | **Version** | **License** | **Dependencies** | +=====================================================================+=============+========================================================================================+======================+ | `pytest `__ | ≥9.0 | `MIT `__ | *Not yet evaluated.* | +---------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `pytest-cov `__ | ≥7.0 | `MIT `__ | *Not yet evaluated.* | +---------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `Coverage `__ | ≥7.13 | `Apache License, 2.0 `__ | *Not yet evaluated.* | +---------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `mypy `__ | ≥1.19 | `MIT `__ | *Not yet evaluated.* | +---------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `typing-extensions `__ | ≥4.15 | `PSF-2.0 `__ | *Not yet evaluated.* | +---------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `lxml `__ | ≥6.0 | `BSD 3-Clause `__ | *Not yet evaluated.* | +---------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ Unit Testing with Benchmarking (Optional) ========================================= Further Python packages are needed for benchmarking. These packages are only needed for developers or on a CI server, thus sub-dependencies are not evaluated further. .. rubric:: Manually Installing Benchmarking Requirements Use the :file:`tests/benchmark/requirements.txt` file to install all dependencies via ``pip3``. The file will recursively install the mandatory dependencies too. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash pip install -U -r tests/benchmark/requirements.txt .. tab-item:: Windows :sync: Windows .. code-block:: powershell pip3 install -U -r tests\benchmark\requirements.txt .. rubric:: Dependency List - With Benchmark Testing +--------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | **Package** | **Version** | **License** | **Dependencies** | +====================================================================+=============+========================================================================================+======================+ | `pytest-benchmark `__ | ≥4.0.0 | `BSD 2-Clause `__ | *Not yet evaluated.* | +--------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ Unit Testing with Performance Testing (Optional) ================================================ Further Python packages are needed for performance testing (comparison). These packages are only needed for developers or on a CI server, thus sub-dependencies are not evaluated further. .. rubric:: Manually Installing Benchmarking Requirements Use the :file:`tests/performance/requirements.txt` file to install all dependencies via ``pip3``. The file will recursively install the mandatory dependencies too. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash pip install -U -r tests/performance/requirements.txt .. tab-item:: Windows :sync: Windows .. code-block:: powershell pip3 install -U -r tests\performance\requirements.txt .. rubric:: Dependency List - With Performance Testing +--------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | **Package** | **Version** | **License** | **Dependencies** | +====================================================================+=============+========================================================================================+======================+ | `anytree `__ | ≥2.13 | `Apache 2 `__ | *Not yet evaluated.* | +--------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `itertree `__ | ≥1.1 | `MIT `__ | *Not yet evaluated.* | +--------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `treelib `__ | ≥1.7 | `Apache 2 `__ | *Not yet evaluated.* | +--------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `networkx `__ | ≥3.4 | `BSD 3-Clause `__ | *Not yet evaluated.* | +--------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ | `igraph `__ | ≥0.11 | `GPL-2.0 `__ | *Not yet evaluated.* | +--------------------------------------------------------------------+-------------+----------------------------------------------------------------------------------------+----------------------+ .. _DEP/documentation: Sphinx Documentation (Optional) ******************************* Additional Python packages needed for documentation generation. These packages are only needed for developers or on a CI server, thus sub-dependencies are not evaluated further. .. rubric:: Manually Installing Documentation Requirements Use the :file:`doc/requirements.txt` file to install all dependencies via ``pip3``. The file will recursively install the mandatory dependencies too. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash pip install -U -r doc/requirements.txt .. tab-item:: Windows :sync: Windows .. code-block:: powershell pip3 install -U -r doc\requirements.txt .. rubric:: Dependency List +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Package** | **Version** | **License** | **Dependencies** | +=================================================================================================+==============+==========================================================================================================+======================================================================================================================================================+ | `pyTooling `__ | ≥8.10 | `Apache License, 2.0 `__ | *None* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `Sphinx `__ | ≥8.2 | `BSD 3-Clause `__ | *Not yet evaluated.* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `sphinx_rtd_theme `__ | ≥3.0 | `MIT `__ | *Not yet evaluated.* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `sphinxcontrib-mermaid `__ | ≥1.0 | `BSD `__ | *Not yet evaluated.* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `autoapi `__ | ≥2.0.1 | `Apache License, 2.0 `__ | *Not yet evaluated.* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `sphinx_design `__ | ≥0.6 | `MIT `__ | *Not yet evaluated.* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `sphinx-copybutton `__ | ≥0.5 | `MIT `__ | *Not yet evaluated.* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `sphinx_autodoc_typehints `__ | ≥3.5 | `MIT `__ | *Not yet evaluated.* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `ruamel.yaml `__ | ≥0.18 | `MIT `__ | *Not yet evaluated.* | +-------------------------------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ .. _DEP/packaging: Packaging (Optional) ******************** Additional Python packages needed for installation package generation. These packages are only needed for developers or on a CI server, thus sub-dependencies are not evaluated further. .. rubric:: Manually Installing Packaging Requirements Use the :file:`build/requirements.txt` file to install all dependencies via ``pip3``. The file will recursively install the mandatory dependencies too. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash pip install -U -r build/requirements.txt .. tab-item:: Windows :sync: Windows .. code-block:: powershell pip3 install -U -r build\requirements.txt .. rubric:: Dependency List +----------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Package** | **Version** | **License** | **Dependencies** | +============================================================================+==============+==========================================================================================================+======================================================================================================================================================+ | `pyTooling `__ | ≥8.10 | `Apache License, 2.0 `__ | *None* | +----------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ | `wheel `__ | ≥0.45 | `MIT `__ | *Not yet evaluated.* | +----------------------------------------------------------------------------+--------------+----------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+ .. _DEP/publishing: Publishing (CI-Server only) *************************** Additional Python packages needed for publishing the generated installation package to e.g, PyPI or any equivalent services. These packages are only needed for maintainers or on a CI server, thus sub-dependencies are not evaluated further. .. rubric:: Manually Installing Publishing Requirements Use the :file:`dist/requirements.txt` file to install all dependencies via ``pip3``. The file will recursively install the mandatory dependencies too. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash pip install -U -r dist/requirements.txt .. tab-item:: Windows :sync: Windows .. code-block:: powershell pip3 install -U -r dist\requirements.txt .. rubric:: Dependency List +----------------------------------------------------------+--------------+-------------------------------------------------------------------------------------------+----------------------+ | **Package** | **Version** | **License** | **Dependencies** | +==========================================================+==============+===========================================================================================+======================+ | `wheel `__ | ≥0.45 | `MIT `__ | *Not yet evaluated.* | +----------------------------------------------------------+--------------+-------------------------------------------------------------------------------------------+----------------------+ | `Twine `__ | ≥6.2 | `Apache License, 2.0 `__ | *Not yet evaluated.* | +----------------------------------------------------------+--------------+-------------------------------------------------------------------------------------------+----------------------+ pyTooling-8.11.0/doc/Doc-License.rst000066400000000000000000000440731513317154500171660ustar00rootroot00000000000000.. _DOCLICENSE: .. note:: This is a local copy of the `Creative Commons - Attribution 4.0 International (CC BY 4.0) `__. .. attention:: This **CC BY 4.0** license applies only to the **documentation** of this project. Creative Commons Attribution 4.0 International ############################################## Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. .. topic:: Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. * **Considerations for licensors:** Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. `More considerations for licensors `__. * **Considerations for the public:** By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. `More considerations for the public `__. :xlarge:`Creative Commons Attribution 4.0 International Public License` By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 – Definitions. ======================== a. **Adapted Material** means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. **Adapter's License** means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. **Copyright and Similar Rights** means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. d. **Effective Technological Measures** means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. e. **Exceptions and Limitations** means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. f. **Licensed Material** means the artistic or literary work, database, or other material to which the Licensor applied this Public License. g. **Licensed Rights** means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. h. **Licensor** means the individual(s) or entity(ies) granting rights under this Public License. i. **Share** means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. j. **Sui Generis Database Rights** means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. k. **You** means the individual or entity exercising the Licensed Rights under this Public License. **Your** has a corresponding meaning. Section 2 – Scope. ================== a. **License grant.** 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: A. reproduce and Share the Licensed Material, in whole or in part; and B. produce, reproduce, and Share Adapted Material. 2. :underline:`Exceptions and Limitations.` For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. :underline:`Term.` The term of this Public License is specified in Section 6(a). 4. :underline:`Media and formats`; :underline:`technical modifications allowed.` The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 5. :underline:`Downstream recipients.` A. :underline:`Offer from the Licensor – Licensed Material.` Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. B. :underline:`No downstream restrictions.` You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. :underline:`No endorsement.` Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. **Other rights.** 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 – License Conditions. =============================== Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. **Attribution.** 1. If You Share the Licensed Material (including in modified form), You must: A. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. Section 4 – Sui Generis Database Rights. ======================================== Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 – Disclaimer of Warranties and Limitation of Liability. ================================================================= a. **Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.** b. **To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.** c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 – Term and Termination. ================================= a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 – Other Terms and Conditions. ======================================= a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 – Interpretation. =========================== a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ------------------ Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at `creativecommons.org/policies `__, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at `creativecommons.org `__ pyTooling-8.11.0/doc/DocCoverage.rst000066400000000000000000000007551513317154500172610ustar00rootroot00000000000000Documentation Coverage ###################### .. grid:: 2 .. grid-item:: :columns: 8 .. report:doc-coverage:: :reportid: src .. grid-item:: :columns: 4 .. report:doc-coverage-legend:: :reportid: src :style: vertical-table ---------- Documentation coverage generated with `docstr-coverage `__ and visualized by `sphinx-reports `__. pyTooling-8.11.0/doc/Exceptions.rst000066400000000000000000000020461513317154500172140ustar00rootroot00000000000000.. _EXECPTION: Overview ######## .. #contents:: Table of Contents :depth: 2 .. _EXECPTION/Base: Exception Base Classes ###################### The :mod:`pyTooling.Exceptions` package provides extensible exceptions. ExceptionBase ************* The :exc:`ExceptionBase` is the base-class for all exceptions in ``pyTooling`` as well as derived packages and frameworks. .. _EXECPTION/Predefined: Predefined Exceptions ##################### Predefined exceptions of ``pyTooling.Exceptions``. .. rubric:: Inheritance diagram: .. inheritance-diagram:: pyTooling.Exceptions :parts: 1 EnvironmentException ******************** .. todo:: EXCEPTION:: Needs documentation for EnvironmentException PlatformNotSupportedException ***************************** .. todo:: EXCEPTION:: Needs documentation for PlatformNotSupportedException NotConfiguredException ********************** .. todo:: EXCEPTION:: Needs documentation for NotConfiguredException .. seealso:: Base excepetion class :exc:`ExceptionBase` Base class for all exceptions. pyTooling-8.11.0/doc/Glossary.rst000066400000000000000000000424721513317154500167050ustar00rootroot00000000000000Glossary ######## .. glossary:: Abstract Class A :wiki:`abstract class ` is a type, that cannot be instantiated directly. An *abstract* class may provide no implementation or an incomplete implementation. In pyTooling such a type is assumed, when a class contains at least one :term:`abstract ` or :term:`mustoverride ` method and pyToolings meta-class :ref:`META/ExtendedType` was applied. If an *abstract* class is instantiated, an exception is raised. Abstract Method An *abstract* method provides no implementation (no code) and must therefore be implemented by all derived classes. If an *abstract* method is called, an exception is raised. Also if, an *abstract* method is not overridden, an exception is raised when instantiating the class, because the :term:`class is abstract `. Ancestor *Ancestors* are all direct and indirect predecessors of a :term:`node` (:term:`parent node ` and parent nodes thereof a.k.a. :term:`grandparents `, grand-grandparent, ..., :term:`root` node). In a tree, a node has only a single parent per node, thus a list of ancestors is a direct line from current node to the root node. .. mermaid:: :caption: Ancestors of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R:::mark1 --> A A:::mark2 --> BL & B & BR B:::mark2 --> CL & C & CR C:::mark2 --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6,font-size:smaller; classDef mark1 fill:#69f,stroke:#37f,color:#eee,font-size:smaller; classDef mark2 fill:#69f,stroke:#37f,font-size:smaller; Base-Class A *base-class* is an ancestor class for other classes derived therefrom. .. mermaid:: :caption: Base-class in a class hierarchy. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD B(BaseClass) C(Class) I1(Instance);I2(Instance) B:::mark1 --> C:::mark2 -..-> I1 & I2 classDef node font-size:smaller; classDef mark1 fill:#69f,stroke:#37f,color:#eee,font-size:smaller; classDef mark2 fill:#69f,stroke:#37f,font-size:smaller; Child *Children* are all direct successors of a :term:`node`. .. mermaid:: :caption: Children of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER EL:::mark2 E:::mark2 ER:::mark2 DR --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark2 fill:#69f,stroke:#37f; CLIOption undocumented CLIParameter undocumented CopyLeft undocumented Wikipedia: :wiki:`Copyleft ` Cygwin :wiki:`Cygwin ` is a :wiki:`POSIX `-compatible programming and runtime environment for Windows. DAG A *directed acyclic graph* (DAG) is a :term:`directed graph ` without backward edges and therefore free of cycles. .. mermaid:: :caption: A directed acyclic graph. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph LR A(A); B(B); C(C); D(D); E(E); F(F); G(G); H(H); I(I); J(J); K(K) A --> B & C & D B --> E & F C --> E & G D --> G & F E --> H F --> H & I G --> I H --> J & K I --> K & J classDef node fill:#eee,stroke:#777,font-size:smaller; DG A *directed graph* (DG) is a :term:`graph` where all :term:`edges ` have a direction. .. mermaid:: :caption: A directed graph with cycles (one cycle is denoted by dotted edges). %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph LR A(A); B(B); C(C); D(D); E(E); F(F) ; G(G); H(H); I(I) A -.-> B -.-> E G --> F A --> C --> G --> H --> D D -.-> A D & F --> B I ---> E -.-> F -.-> D classDef node fill:#eee,stroke:#777,font-size:smaller; Decorator undocumented Descendant *Descendants* are all direct and indirect successors of a :term:`node` (:term:`child nodes ` and child nodes thereof a.k.a. :term:`grandchild`, grand-grandchildren, ...). .. mermaid:: :caption: Descendants of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER EL:::mark2 E:::mark2 ER:::mark2 DR --> ERN1 & ERN2 E --> F1 & F2 F1:::mark2 F2:::mark2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark2 fill:#69f,stroke:#37f; Edge An *edge* is a relation from :term:`vertex` to vertex in a :term:`graph`. Executable undocumented Exception undocumented Graph A *graph* is a data structure made of :term:`vertices ` (nodes) and vertex-vertex relations called :term:`edges `. Special forms of graphs are: * Graphs with directions: :term:`Directed Graph ` * Directed Graphs without Cycles: :term:`Directed Acyclic Graph ` * Directed Acyclic Graph without Side-Edges: :term:`Tree` .. mermaid:: :caption: A directed graph with backward-edges denoted by dotted vertex relations. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph LR A(A); B(B); C(C); D(D); E(E); F(F) ; G(G); H(H); I(I) A --> B --> E G --> F A --> C --> G --> H --> D D -.-> A D & F -.-> B I ---> E --> F --> D classDef node fill:#eee,stroke:#777,font-size:smaller; Grandchild *Grandchildren* are direct successors of a node's :term:`children ` and therefore indirect successors of a :term:`node`. .. mermaid:: :caption: Grandchildren of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 F1:::mark2 F2:::mark2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark2 fill:#69f,stroke:#37f; Grandparent A *grandparent* is direct predecessor of a node's :term:`parent` and therefore indirect predecessor of a :term:`node`. .. mermaid:: :caption: Grandparent of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B:::mark2 --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark2 fill:#69f,stroke:#37f; Hardlink undocumented Meta-Class A *meta-class* is a class helping to construct classes. Thus, it's the type of a type. .. mermaid:: :caption: Relation of meta-classes, classes and instances. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD T(type) ET(MetaClass) B(BaseClass) M(MixIn) C(Class) I1(Instance);I2(Instance) T --> T T:::mark1 --> ET:::mark1 -.class definition.-> B B:::mark2 --inheritance--> C:::mark2 -.instantiation..-> I1 & I2 M --inheritance--> C classDef node font-size:smaller; classDef mark1 fill:#69f,stroke:#37f,color:#eee,font-size:smaller; classDef mark2 fill:#69f,stroke:#37f,font-size:smaller; MinGW Minimalistic GNU for Windows. Wikipedia: :wiki:`MinGW ` Mixin-Class A *mixin classes* are classes used as secondary base-classes in multiple inheritance. MSYS2 undocumented Wikipedia: :wiki:`MSYS2 ` Mustoverride Method A *must-override* method provides a partial implementation (incomplete code) and must therefore be fully implemented by all derived classes. If a *must-override* method is not overridden, an exception is raised when instantiating the class, because the :term:`class is abstract `. native A *native environment* is a platform just with the operating system. There is no additional environment layer like MSYS2. Node undocumented Overloading undocumented Parent A *parent* is direct predecessor of a :term:`node`. .. mermaid:: :caption: Parent of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C:::mark2 --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark2 fill:#69f,stroke:#37f; Post-Order undocumented Pre-Order undocumented Program undocumented PyPI undocumented Wikipedia: :wiki:`Python Package Index ` PyPy undocumented Wikipedia: :wiki:`PyPy ` Relative *Relatives* are :term:`siblings ` and their :term:`descendants `. Left relatives are left siblings and all their descendants, whereas right relatives are right siblings and all their descendants. .. mermaid:: :caption: Relatives of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL:::mark2 --> ELN1 & ELN2 ELN1:::mark2 ELN2:::mark2 D:::cur --> EL & E & ER DR:::mark2 --> ERN1 & ERN2 ERN1:::mark2 ERN2:::mark2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark2 fill:#69f,stroke:#37f; Root All :term:`nodes ` in a :term:`tree` have one common :term:`ancestor` called *root*. .. mermaid:: :caption: Root of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R:::mark1 --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; Sibling *Siblings* are all direct :term:`child nodes ` of a node's :term:`parent` node except itself. .. mermaid:: :caption: Siblings of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL:::mark2 --> ELN1 & ELN2 D:::cur --> EL & E & ER DR:::mark2 --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark2 fill:#69f,stroke:#37f; Singleton The :wiki:`singleton design pattern ` ensures only a single instance of a class to exist. If another instance is going to be created, a previously cached instance of that class will be returned. Slots undocumented Softlink undocumented Tree A *tree* is a data structure made of :term:`nodes ` and parent-child relations. All nodes in a tree share one common :term:`ancestor` call :term:`root`. A tree is a special form of a :term:`directed acyclic graph (DAG) `. UCRT Universal C Runtime Wikipedia: :wiki:`Microsoft Windows library files: UCRT ` URI Uniform Resource Identifier Wikipedia: :wiki:`Uniform Resource Identifier ` URL Uniform Resource Locator Wikipedia: :wiki:`Uniform Resource Locator ` URN Uniform Resource Name Wikipedia: :wiki:`Uniform Resource Name ` Vertex A vertex is a :term:`node` in a graph. Vertexes in a graph are connected using :term:`edges `. WSL Windows System for Linux Wikipedia: :wiki:`Windows Subsystem for Linux ` pyTooling-8.11.0/doc/Installation.rst000066400000000000000000000371541513317154500175440ustar00rootroot00000000000000.. |PackageName| replace:: pyTooling .. _INSTALL: Installation/Updates #################### See the following instructions on how to install or update the package from common sources like PyPI. Developers can also install the packages with development dependencies. In case of local development, see the additional sections on how to run unit tests, type checks or how to build the documentation to create all the build artifacts. See the list of :ref:`necessary dependencies `. .. _INSTALL/pip: Using PIP to Install from PyPI ****************************** The following instruction are using PIP (Package Installer for Python) as a package manager and PyPI (Python Package Index) as a source of Python packages. PIP might download further packages as listed in :ref:`package dependencies `. .. _INSTALL/pip/install: Installing a Wheel Package from PyPI using PIP ============================================== Users can install the |PackageName| package as a minimal installation or the package with extensions (``packaging``, ``terminal``, ``yaml``) installing further dependencies. In case the provided extensions are not needed, it keeps the list of dependencies low - especially the minimal installation is still dependency free. See :ref:`DEP/package` for more details. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. tab-set:: .. tab-item:: Minimal installation :sync: Minimal .. code-block:: bash # Basic pyTooling package pip3 install pyTooling # Alternatively python3 -m pip install pyTooling .. tab-item:: With Packaging Support :sync: Packaging .. code-block:: bash # With setuptools support for pyTooling.Packaging pip3 install pyTooling[packaging] # Alternatively python3 -m pip install pyTooling[packaging] .. tab-item:: With Colored Console/Terminal Support :sync: Terminal .. code-block:: bash # With color support for pyTooling.TerminalUI pip3 install pyTooling[terminal] # Alternatively python3 -m pip install pyTooling[terminal] .. tab-item:: With YAML Support for Configuration Files :sync: YAML .. code-block:: bash # With YAML support for pyTooling.Configuration.YAML pip3 install pyTooling[yaml] # Alternatively python3 -m pip install pyTooling[yaml] .. tab-item:: Windows :sync: Windows .. tab-set:: .. tab-item:: Minimal installation :sync: Minimal .. code-block:: powershell # Basic pyTooling package pip install pyTooling # Alternatively py -m pip install pyTooling .. tab-item:: With Packaging Support :sync: Packaging .. code-block:: powershell # With setuptools support for pyTooling.Packaging pip install pyTooling[packaging] # Alternatively py -m pip install pyTooling[packaging] .. tab-item:: With Colored Console/Terminal Support :sync: Terminal .. code-block:: powershell # With color support for pyTooling.TerminalUI pip install pyTooling[terminal] # Alternatively py -m pip install pyTooling[terminal] .. tab-item:: With YAML Support for Configuration Files :sync: YAML .. code-block:: powershell # With YAML support for pyTooling.Configuration.YAML pip install pyTooling[yaml] # Alternatively py -m pip install pyTooling[yaml] Developers can install the |PackageName| package itself or the package with further dependencies for documentation generation (``doc``), running unit tests (``test``) or just all (``all``) dependencies. See :ref:`DEP` for more details. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. tab-set:: .. tab-item:: Minimal installation :sync: Minimal .. code-block:: bash # Basic pyTooling package pip3 install pyTooling # Alternatively python3 -m pip install pyTooling .. tab-item:: With Documentation Dependencies :sync: Doc .. code-block:: bash # Install with dependencies to generate documentation pip3 install pyTooling[doc] # Alternatively python3 -m pip install pyTooling[doc] .. tab-item:: With Unit Testing Dependencies :sync: Unit .. code-block:: bash # Install with dependencies to run unit tests pip3 install pyTooling[test] # Alternatively python3 -m pip install pyTooling[test] .. tab-item:: All Developer Dependencies :sync: All .. code-block:: bash # Install with all developer dependencies pip3 install pyTooling[all] # Alternatively python3 -m pip install pyTooling[all] .. tab-item:: Windows :sync: Windows .. tab-set:: .. tab-item:: Minimal installation :sync: Minimal .. code-block:: powershell # Basic pyTooling package pip install pyTooling # Alternatively py -m pip install pyTooling .. tab-item:: With Documentation Dependencies :sync: Doc .. code-block:: powershell # Install with dependencies to generate documentation pip install pyTooling[doc] # Alternatively py -m pip install pyTooling[doc] .. tab-item:: With Unit Testing Dependencies :sync: Unit .. code-block:: powershell # Install with dependencies to run unit tests pip install pyTooling[test] # Alternatively py -m pip install pyTooling[test] .. tab-item:: All Developer Dependencies :sync: All .. code-block:: powershell # Install with all developer dependencies pip install pyTooling[all] # Alternatively py -m pip install pyTooling[all] .. _INSTALL/pip/requirements: Referencing the package in ``requirements.txt`` =============================================== When |PackageName| is used by another Python package, it's recommended to list the dependency to the |PackageName| package in a ``requirements.txt`` file. .. admonition:: ``requirements.txt`` .. code-block:: text pyTooling ~= 8.10 .. _INSTALL/pip/update: Updating from PyPI using PIP ============================ .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash # Update pyTooling pip3 install -U pyTooling # Alternatively python3 -m pip install -U pyTooling .. tab-item:: Windows :sync: Windows .. code-block:: powershell # Update pyTooling pip install -U pyTooling # Alternatively py -m pip install -U pyTooling .. _INSTALL/pip/uninstall: Uninstallation using PIP ======================== .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash # Uninstall pyTooling pip3 uninstall pyTooling # Alternatively python3 -m pip uninstall pyTooling .. tab-item:: Windows :sync: Windows .. code-block:: powershell # Uninstall pyTooling pip uninstall pyTooling # Alternatively py -m pip uninstall pyTooling .. _INSTALL/testing: Running unit tests ****************** This package is provided with unit tests for `pytest `__. The provided testcases can be executed locally for testing or development purposes. In addition, code coverage including branch coverage can be collected using `Coverage.py `__. All steps provide appropriate artifacts as XML or HTML reports. The artifact output directories are specified in ``pyproject.toml``. Ensure :ref:`unit testing requirements ` are installed. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. tab-set:: .. tab-item:: Unit Testing :sync: UnitTesting .. code-block:: bash cd # Running unit tests using pytest pytest -raP --color=yes tests/unit .. tab-item:: Unit Testing with Ant/JUnit XML Reports :sync: UnitTestingXML .. code-block:: bash cd # Running unit tests using pytest pytest -raP --color=yes --junitxml=report/unit/unittest.xml --template=html1/index.html --report=report/unit/html/index.html --split-report tests/unit .. tab-item:: Unit Testing with Code Coverage :sync: Coverage .. code-block:: bash cd # Running unit tests with code coverage using Coverage.py coverage run --data-file=.coverage --rcfile=pyproject.toml -m pytest -ra --tb=line --color=yes tests/unit # Write coverage report to console" coverage report # Convert coverage report to HTML coverage html # Convert coverage report to XML (Cobertura) coverage xml .. tab-item:: Windows :sync: Windows .. tab-set:: .. tab-item:: Unit Testing :sync: UnitTesting .. code-block:: powershell cd # Running unit tests using pytest pytest -raP --color=yes tests\unit .. tab-item:: Unit Testing with Ant/JUnit XML Reports :sync: UnitTestingXML .. code-block:: powershell cd # Running unit tests using pytest pytest -raP --color=yes --junitxml=report\unit\unittest.xml --template=html1\index.html --report=report\unit\html\index.html --split-report tests\unit .. tab-item:: Unit Testing with Code Coverage :sync: Coverage .. code-block:: powershell cd # Running unit tests with code coverage using Coverage.py coverage run --data-file=.coverage --rcfile=pyproject.toml -m pytest -ra --tb=line --color=yes tests\unit # Write coverage report to console" coverage report # Convert coverage report to HTML coverage html # Convert coverage report to XML (Cobertura) coverage xml .. _INSTALL/typechecking: Running type checks ******************* This package is provided with type checks. These can be executed locally for testing or development purposes using `mypy `__. The artifact output directory is specified in ``pyproject.toml``. Ensure :ref:`unit testing requirements ` are installed. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash cd # Running type checking using mypy export MYPY_FORCE_COLOR=1 mypy -p pyTooling .. tab-item:: Windows :sync: Windows .. code-block:: powershell cd # Running type checking using mypy $env:MYPY_FORCE_COLOR = 1 mypy -p pyTooling .. _INSTALL/documentation: Building documentation ********************** The documentation can be build locally using `Sphinx `__. It can generate HTML and LaTeX outputs. In an additional step, the LaTeX output can be translated to a PDF file using a LaTeX environment like `MiKTeX `__. Ensure :ref:`documentation requirements ` are installed. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. tab-set:: .. tab-item:: Generating HTML :sync: HTML .. code-block:: bash cd # Adding package root to PYTHONPATH export PYTHONPATH=$(pwd) cd doc # Building documentation using Sphinx sphinx-build -v -n -b html -d _build/doctrees -j $(nproc) -w _build/html.log . _build/html .. tab-item:: Generating LaTeX :sync: LaTeX .. code-block:: bash cd # Adding package root to PYTHONPATH export PYTHONPATH=$(pwd) cd doc # Building documentation using Sphinx sphinx-build -v -n -b latex -d _build/doctrees -j $(nproc) -w _build/latex.log . _build/latex .. tab-item:: Generating PDF (from LaTeX) :sync: PDF .. todo:: Describe LaTeX to PDF conversion on Linux using Miktex. .. hint:: A `Miktex installation `__ is required. .. tab-item:: Windows :sync: Windows .. tab-set:: .. tab-item:: Generating HTML :sync: HTML .. code-block:: powershell cd # Building documentation using Sphinx .\doc\make.bat html --verbose .. tab-item:: Generating LaTeX :sync: LaTeX .. code-block:: powershell cd # Building documentation using Sphinx .\doc\make.bat latex --verbose .. tab-item:: Generating PDF (from LaTeX) :sync: PDF .. todo:: Describe LaTeX to PDF conversion on Windows using Miktex. .. hint:: A `Miktex installation `__ is required. .. _INSTALL/building: Local Packaging and Installation via PIP **************************************** For development and bug fixing it might be handy to create a local wheel package and also install it locally on the development machine. The following instructions will create a local wheel package (``*.whl``) and then use PIP to install it. As a user might have a |PackageName| installation from PyPI, it's recommended to uninstall any previous |PackageName| packages. (This step is also needed if installing an updated local wheel file with same version number. PIP will not detect a new version and thus not overwrite/reinstall the updated package contents.) Ensure :ref:`packaging requirements ` are installed. .. tab-set:: .. tab-item:: Linux/macOS :sync: Linux .. code-block:: bash cd # Package the code in a wheel (*.whl) python3 -m build --wheel # Uninstall the old package python3 -m pip uninstall -y pyTooling # Install from wheel python3 -m pip install ./dist/pyTooling-8.11.0-py3-none-any.whl .. tab-item:: Windows :sync: Windows .. code-block:: powershell cd # Package the code in a wheel (*.whl) py -m build --wheel # Uninstall the old package py -m pip uninstall -y pyTooling # Install from wheel py -m pip install .\dist\pyTooling-8.11.0-py3-none-any.whl .. note:: The legacy ways of building a package using ``setup.py bdist_wheel`` and installation using ``setup.py install`` is not recommended anymore. pyTooling-8.11.0/doc/License.rst000066400000000000000000000254061513317154500164620ustar00rootroot00000000000000.. _SRCLICENSE: .. note:: This is a local copy of the `Apache License Version 2.0 `__. .. attention:: This **Apache License, 2.0** applies to all **source and configuration files of project**, **except documentation**. Apache License 2.0 ################## Version 2.0, January 2004 :xlarge:`TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION` 1. Definitions. =============== **"License"** shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. **"Licensor"** shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. **"Legal Entity"** shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, **"control"** means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. **"You"** (or **"Your"**) shall mean an individual or Legal Entity exercising permissions granted by this License. **"Source"** form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. **"Object"** form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. **"Work"** shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). **"Derivative Works"** shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. **"Contribution"** shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, **"submitted"** means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as **"Not a Contribution."** **"Contributor"** shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. ============================== Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. =========================== Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. ================== You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: * You must give any other recipients of the Work or Derivative Works a copy of this License; and * You must cause any modified files to carry prominent notices stating that You changed the files; and * You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and * If the Work includes a **"NOTICE"** text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. =============================== Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. ============== This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. ========================== Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. =========================== In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. ============================================== While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ---------------------------------------------------------------------------------------------------------------------------------------------------------------- :xlarge:`Appendix: How to apply the Apache License to your work` To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. .. code-block:: none Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pyTooling-8.11.0/doc/Makefile000066400000000000000000000011721513317154500160000ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pyTooling-8.11.0/doc/MetaClasses.rst000066400000000000000000000143511513317154500173010ustar00rootroot00000000000000.. _META: Overview ######## Currently, the following meta-classes are provided: .. #contents:: Table of Contents :depth: 3 .. seealso:: Meta Classes `Understanding Python metaclasses `__ Python Data Model General :ref:`Data Model ` of Python and section about :ref:`__slots__ `. .. _META/ExtendedType: ExtendedType ############ The new meta-class :class:`~pyTooling.MetaClasses.ExtendedType` allows to implement :ref:`singletons `, :ref:`slotted types ` and combinations thereof. Since Python 3, meta-classes are applied in a class definition by adding a named parameter called ``metaclass`` to the list of derived classes (positional parameters). Further named parameters might be given to pass parameters to that new meta-class. .. code-block:: python class MyClass(metaclass=ExtendedType): pass .. _META/Slotted: Slotted ******* .. _META/Mixin: Mixin ***** .. _META/Abstract: Abstract Method *************** The :func:`~pyTooling.MetaClasses.abstractmethod` decorator marks a method as *abstract*. The original method gets replaced by a method raising a :exc:`NotImplementedError`. When a class containing *abstract* methods is instantiated, an :exc:`~pyTooling.Exceptions.AbstractClassError` is raised. .. rubric:: Example: .. code-block:: Python class A(metaclass=ExtendedType): @abstractmethod def method(self) -> int: """Methods documentation.""" class B(A): @InheritDocString(A) def method(self) -> int: return 2 .. hint:: If the abstract method should contain code that should be called from an overriding method in a derived class, use the :ref:`@mustoverride ` decorator. .. _META/MustOverwrite: MustOverwrite Method ******************** The :func:`~pyTooling.MetaClasses.mustoverride` decorator marks a method as *must override*. When a class containing *must override* methods is instantiated, an :exc:`~pyTooling.Exceptions.MustOverrideClassError` is raised. In contrast to :ref:`@abstractmethod `, the method can still be called from a derived class implementing an overridden method. .. rubric:: Example: .. code-block:: Python class A(metaclass=ExtendedType): @mustoverride def method(self) -> int: """Methods documentation.""" return 2 class B(A): @InheritDocString(A) def method(self) -> int: result = super().method() return result + 1 .. hint:: If the method contain no code and throw an exception when called, use the :ref:`@abstractmethod ` decorator. .. _META/Singleton: Singleton ********* A class defined with enabled ``singleton`` behavior implements the `singleton design pattern `__, which allows only a single instance of that class to exist. If another instance is going to be created, a previously cached instance of that class will be returned. .. code-block:: python class MyClass(metaclass=ExtendedType, singleton=True): pass .. admonition:: Example Usage .. code-block:: python class Terminal(metaclass=ExtendedType, singleton=True): def __init__(self) -> None: pass def WriteLine(self, message): print(message) .. _META/Slottedd: Slotted Type ************ A class defined with enabled ``slots`` behavior stores instance fields in slots. The meta-class, translates all type-annotated fields in a class definition into slots. Slots allow a more efficient field storage and access compared to dynamically stored and accessed fields hosted by ``__dict__``. This improves the memory footprint as well as the field access performance of all class instances. This behavior is automatically inherited to all derived classes. .. code-block:: python class MyClass(metaclass=ExtendedType, slots=True): pass .. admonition:: Example Usage .. code-block:: python class Node(metaclass=ExtendedType, slots=True): _parent: "Node" def __init__(self, parent: "Node" = None) -> None: self._parent = parent root = Node() node = Node(root) .. _META/SlottedObject: SlottedObject ============= A class definition deriving from :class:`~pyTooling.MetaClasses.SlottedObject` will bring the slotted type behavior to that class and all derived classes. +----------------------------------------+----------------------------------------+----------------------------------------------------------+ | Deriving from ``SlottedObject`` | Apply ``slotted`` Decorator | Deriving from ``SlottedObject`` | +========================================+========================================+==========================================================+ | .. code-block:: Python | .. code-block:: Python | .. code-block:: Python | | | | | | class MyClass(SlottedObject): | @slotted | class MyClass(metaclass=ExtendedType, slots=True): | | pass | class MyClass(SlottedObject): | pass | | | pass | | +----------------------------------------+----------------------------------------+----------------------------------------------------------+ .. _META/Overloading: Overloading ########### .. warning:: This needs a clear definition before overloading makes sense... This class provides a method dispatcher based on method signature's type annotations. .. admonition:: Example Usage .. code-block:: python class A(metaclass=Overloading): value = None def __init__(self, value : int = 0) -> None: self.value = value def __init__(self, value : str) -> None: self.value = int(value) a = A() print(a.value) b = A(3) print(b.value) c = A("42") print(c.value) pyTooling-8.11.0/doc/News.rst000066400000000000000000000427671513317154500160250ustar00rootroot00000000000000.. _NEWS: News #### See `pyTooling Release Pages `__ for detail release notes on every release. Version 8.x (2025) ****************** .. topic:: `v8.11.0 - 18.01.2026 `__ tbd .. topic:: `v8.10.0 - 09.01.2026 `__ tbd .. topic:: `v8.9.1 - 08.01.2026 `__ tbd .. topic:: `v8.9.0 - 08.01.2026 `__ tbd .. topic:: `v8.8.0 - 10.11.2025 `__ tbd .. topic:: `v8.7.4 - 19.10.2025 `__ tbd .. topic:: `v8.7.3 - 21.09.2025 `__ tbd .. topic:: `v8.7.2 - 04.09.2025 `__ tbd .. topic:: `v8.7.1 - 04.09.2025 `__ tbd .. topic:: `v8.7.0 - 24.08.2025 `__ tbd .. topic:: `v8.6.0 - 12.08.2025 `__ tbd .. topic:: `v8.5.1 - 14.06.2025 `__ tbd .. topic:: `v8.5.0 - 31.05.2025 `__ tbd .. topic:: `v8.4.0 - 25.01.2025 `__ tbd .. topic:: `v8.3.0 - 25.01.2025 `__ * New count function to count the number of elements in an iterator/generator. * Added __setitem__ on pyTooling.CLIAbstraction.Environment. * Added __delitem__ on pyTooling.CLIAbstraction.Environment. .. topic:: `v8.2.0 - 25.01.2025 `__ * Add WarningCollector to handle warnings similar to exceptions and send them along the call stack. .. topic:: `v8.1.0 - 25.01.2025 `__ * Graph * Added methods HasVertexByID, HasVertexByValue. * Added method GetVertexByValue. * Versioning * Version classes are now hashable. * Added gamma release level. * Stopwatch * Added Exclude context manager .. topic:: `v8.0.0 - 09.11.2024 `__ * Reworked semantic and calendar version classes: * Moved common implementations to Version base-type. * Moved major, minor, micro, build, post, dev, release level, release number, hash, prefix, postfix parts to the base-type. * Moved implementations of comparison operators to the base-type: __eq__, __ne__, __lt__, __le__, __gt__, __ge__. * Implemented minimum comparison operator using __rshift__ (>>) for PIP's ~= operator. * Implemented a formatting helper method _format. * Reworked SemanticVersion. * Additionally allow comparisons with string and integer types. * Enhanced SemanticVersion.Parse() class-method: * Raise exceptions on invalid inputs. * Use a regular expression to check and split the input. * Implemented CalendarVersion (previously a dummy). * Added CalendarVersion.Parse() class-method: raise exceptions on invalid inputs. * Implemented comparison operators. * Added validator classes WordSizeValidator and MaxValueValidator. * Added doc-strings. * Improved __str__() method to return only used version parts. * Added __format__() for user defined formatting specifications. Version 7.x (2024) ****************** .. topic:: `v7.0.0 - 27.10.2024 `__ * Added support for Python 3.13 (and dropped 3.8). * Changed DEFAULT_PY_VERSIONS in pyTooling.Packaging to 3.9...3.13. * Reworked faulty Timer class and renamed it to StopWatch. * Support start, pause, resume, split and stop operations. * Collect active and inactive split times. * Accept a name at instantiation. * Take absolute time at start and stop via datetime.now(). * Can be used in a with-statement. * @InheritDocString can be applied to classes too. Version 6.x (2024) ****************** .. topic:: `v6.7.0 - 29.09.2024 `__ * :mod:`pyTooling.Terminal` * Added TerminalApplication.WriteCritical * Added TerminalApplication.ExitOnPreviousCriticalWarnings .. topic:: `v6.6.0 - 18.09.2024 `__ * :mod:`pyTooling.Graph` * Allow setting key-value-pairs for a graph when creating a new graph. * Allow setting key-value-pairs for vertices when creating a new vertex. * Allow setting key-value-pairs for edges when creating a new edge. * Allow setting key-value-pairs for links when creating a new link. * :mod:`pyTooling.Packaging` * :func:`~pyTooling.Packaging.loadReadmeFile` now supports new content formats: * plain text * ReStructured Text * :mod:`pyTooling.Platform` * Added :attr:`~pyTooling.Platform.Platform.StaticLibraryExtension`. .. topic:: `v6.5.0 - 15.07.2024 `__ * :mod:`pyTooling.GenericPath` * :class:`pyTooling.GenericPath.URL.URL`: * Added support for basic authentication credentials (username and password). * Added :meth:`pyTooling.GenericPath.URL.URL.WithoutCredentials` method. .. topic:: `v6.4.0 - 04.07.2024 `__ * :mod:`pyTooling.Platform` * Added readonly property :attr:`~pyTooling.Platform.Platform.IsNativeFreeBSD` to class Platform. .. topic:: `v6.3.0 - 02.06.2024 `__ * :mod:`pyTooling.Tree` * Accept a custom formatting function per node to return a one-liner representation of a node for tree rendering. * Accept a key-value-pair mapping (dictionary) for nodes in a tree in the initializer. * :mod:`pyTooling.Graph` * Accept a key-value-pair mapping (dictionary) for all data structures (graph, edges, links, vertices, views, ...) in a graph in their initializers. .. topic:: `v6.2.0 - 30.05.2024 `__ * :mod:`pyTooling.Common` * New helper function :func:`pyTooling.Common.getFullyQualifiedName`. * Python 3.8+: New helper functions :func:`pyTooling.Common.getResourceFile` and :func:`pyTooling.Common.readResourceFile`. * Python 3.11+: In case of :class:`TypeError` add a note to the exception describing the parameter/member type. .. topic:: `v6.1.0 - 09.04.2024 `__ .. #empty .. topic:: `v6.0.0 - 14.01.2024 `__ * Integrated ``pyAttributes`` v2.5.1 as :mod:`pyTooling.Attributes`. * Integrated :mod:`pyTooling.CLIAbstraction` v0.4.1. Version 5.x (2023) ****************** .. topic:: `v5.0.0 - 02.07.2023 `__ * New ``ExtendedType`` features: * Added support for mixin-classes and delayed creation of slots. * Added automatic initializers for annotated fields (previously causing an exception due to slots). * Added automatic initializers for annotated class fields (previously causing an exception due to slots). * Added new decorators: ``@slotted``, ``@mixin``, ``@singleton``, ``@readonly``, and ``@notimplemented``. * Added JSON support for ``pyTooling.Configuration``. * New ``Platform`` features: * Added ``PythonVersion`` to ``Platform`` to distinguish Python versions. * Added ``PythonImplementation`` to ``Platform`` to distinguish CPython and PyPy. * New graph features: * ``GetVertexByID`` * ``GetVertexByValue`` * New vertex operations: ``IterateAllOutboundPathsAsVertexList``, ``Delete`` (itself), ``DeleteEdgeTo``, ``DeleteEdgeFrom``, ``DeleteLinkTo``, ``DeleteLinkFrom``. * New edge operations: ``Delete`` (itself) * New link operations: ``Delete`` (itself) * ``pyToolong.StateMachine`` package (alpha version). Version 4.x (2023) ****************** .. topic:: `v4.0.1 - 26.03.2023 `__ * Graphs are now supporting subgraphs and exporting subgraphs to GraphML. * New ``SubGraph`` class. * New ``Link`` class. * New ``View`` class. * Added ``Vertex.Link***Vertex`` methods to link vertices from disjunctive subgraphs. * Added ``Vertex.HasLink***Vertex`` methods check if two vertices from disjunctive subgraphs are connected. * Added ``Vertex.Iterate***boundLinks`` to iterate links. * Added ``Graph.IterateLinks`` to iterate all links. * Added ``Graph.ReverseLinks``, ``Graph.RemoveLinks``. * Applied generic types when deriving from subclasses. * Added ``in`` operator for key-value Version 3.x (2023) ****************** .. topic:: `v3.0.0 - 10.03.2023 `__ * Integrated :mod:`pyTooling.TerminalUI`. * Support for FreeBSD in ``Platform``. * A data model for GraphML (graph, node, edge, key, data and subgraph). * A conversion from pyTooling's graph data structure to GraphML XML files. * A conversion from pyTooling's tree data structure to GraphML XML files. Jan. 2023 - Graph enhancements ****************************** * Improved exceptions. * Added ``ConvertToTree`` method to ``Vertex``. * Added ``Render`` method to ``Node``. Nov. 2023 - Graph implementation ******************************** * Added an object-oriented graph implementation. Archive ******* Attributes ========== .. only:: html Jan. 2024 - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: Jan. 2024 - Direct integration into pyTooling * The standalone package ``pyAttributes`` v2.5.1 has been integrated as :mod:`pyTooling.Attributes` into pyTooling v6.0.0. .. only:: html Nov. 2021 - Moved to pyTooling ------------------------------ .. only:: latex .. rubric:: Nov. 2021 - Moved to pyTooling * Changed repository location from ``Paebbels/pyAttributes`` to ``pyTooling/pyAttributes``. .. only:: html Jan. 2020 - Enhancements ------------------------ .. only:: latex .. rubric:: Jan. 2020 - Enhancements * ``GetMethods`` and ``GetAttributes`` adhere to method resolution order (MRO) to find attributes annotated to methods from base-classes. * An ``AttributeHelperMixinclass`` to ease the usage of attributes on a class' methods. .. only:: html Dec. 2019 - Merge from IPCMI ---------------------------- .. only:: latex .. rubric:: Dec. 2019 - Merge from IPCMI * Merged latest implementation updates from pyIPCMI. .. only:: html Oct. 2019 - Initial Release --------------------------- .. only:: latex .. rubric:: Oct. 2019 - Initial Release * Basic attribute class. * Attribute helper classes. * Package for handling Python's argparse as declarative code. CallByRef ========= .. only:: html xxx. 20XX - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: xxx. 20XX - Direct integration into pyTooling * The namespace package ``pyTooling.CallByRef`` v1.2.1 has been integrated as :mod:`pyTooling.CallByRef` into pyTooling vX.X.X. .. only:: html Sep. 2020 - Bug Fixes --------------------- .. only:: latex .. rubric:: Sep. 2020 - IBug Fixes * Some bugfixes. .. only:: html Dec. 2019 - Initial Release --------------------------- .. only:: latex .. rubric:: Dec. 2019 - Initial Release * Call-by-reference implementation for Python. CLIAbstraction ============== .. only:: html Jan. 2024 - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: Jan. 2024 - Direct integration into pyTooling * The namespace package ``pyTooling.CLIAbstraction`` v0.4.1 has been integrated as :mod:`pyTooling.CLIAbstraction` into pyTooling v6.0.0. .. only:: html Feb. 2022 - Major Update ------------------------ .. only:: latex .. rubric:: Major Update * Reworked names of Argument classes. * Added missing argument formats like PathArgument. * Added more unit tests and improved code-coverage. * Added doc-strings and extended documentation pages. .. only:: html Dec. 2021 - Extracted CLIAbstraction from pyIPCMI ------------------------------------------------- .. only:: latex .. rubric:: Extracted CLIAbstraction from pyIPCMI * The CLI abstraction has been extracted from `pyIPCMI `__. CommonClasses ============= .. only:: html xxx. 20XX - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: xxx. 20XX - Direct integration into pyTooling * The namespace package ``pyTooling.CommonClasses`` v0.2.3 has been integrated as :mod:`pyTooling.CommonClasses` into pyTooling vX.X.X. .. only:: html Feb. 2021 - Initial Release --------------------------- .. only:: latex .. rubric:: Feb. 2021 - Initial Release * Added ``Version`` class. Exceptions ========== .. only:: html xxx. 20XX - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: xxx. 20XX - Direct integration into pyTooling * The namespace package ``pyTooling.Exceptions`` v1.1.1 has been integrated as :mod:`pyTooling.Exceptions` into pyTooling vX.X.X. .. only:: html Sep. 2020 - Unit tests ---------------------- .. only:: latex .. rubric:: Sep. 2020 - Unit tests * Added unit tests. .. only:: html Oct. 2019 - Initial Release --------------------------- .. only:: latex .. rubric:: Oct. 2019 - Initial Release * An initial set of exceptions has been extracted from `pyIPCMI `__. GenericPath =========== .. only:: html xxx. 20XX - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: xxx. 20XX - Direct integration into pyTooling * The namespace package ``pyTooling.GenericPath`` v0.2.5 has been integrated as :mod:`pyTooling.GenericPath` into pyTooling vX.X.X. .. only:: html Dec. 2021 - Namespace package ----------------------------- .. only:: latex .. rubric:: Dec. 2021 - Namespace package * Renamed ``pyGenericPath`` to :mod:`pyTooling.GenericPath`. .. only:: html Oct. 2019 - Initial Release --------------------------- .. only:: latex .. rubric:: Oct. 2019 - Initial Release * An initial set of exceptions has been extracted from `pyIPCMI `__. MetaClasses =========== .. only:: html xxx. 20XX - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: xxx. 20XX - Direct integration into pyTooling * The namespace package ``pyTooling.MetaClasses`` v1.3.1 has been integrated as :mod:`pyTooling.MetaClasses` into pyTooling vX.X.X. .. only:: html Aug. 2020 - Overloading ----------------------- .. only:: latex .. rubric:: Aug. 2020 - Overloading * First implementation of method overloading via a meta-class. .. only:: html Dec. 2019 - Initial Release --------------------------- .. only:: latex .. rubric:: Dec. 2019 - Initial Release * First singleton metaclass to implement the singleton pattern in Python. Packaging ========= .. only:: html Dec. 2021 - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: Dec. 2021 - Direct integration into pyTooling * The namespace package ``pyTooling.Packaging`` v0.5.0 has been integrated as :mod:`pyTooling.Packaging` into pyTooling vX.X.X. .. only:: html Nov. 2021 - Major enhancements ------------------------------ .. only:: latex .. rubric:: Nov. 2021 - Major enhancements * Reading package information from Python source code via Python's AST. * Support more licenses. .. only:: html Nov. 2021 - Initial Release --------------------------- .. only:: latex .. rubric:: Nov. 2021 - Initial Release * Abstract setuptools.setup to ease handling of Python package descriptions. * Read long description from README.md * Read package dependencies from requirements.txt * Construct classifiers * Construct URLs for packages hosted on GitHub. TerminalUI ========== .. only:: html xxx. 20XX - Direct integration into pyTooling --------------------------------------------- .. only:: latex .. rubric:: xxx. 20XX - Direct integration into pyTooling * The namespace package ``pyTooling.TerminalUI`` v1.5.9 has been integrated as :mod:`pyTooling.TerminalUI` into pyTooling vX.X.X. .. only:: html Nov. 2021 - Namespace package ----------------------------- .. only:: latex .. rubric:: Nov. 2021 - Namespace package * Renamed ``pyTerminalUI`` to :mod:`pyTooling.TerminalUI`. .. only:: html Aug. 2020 - Enhancements ------------------------ .. only:: latex .. rubric:: Aug. 2020 - Enhancements * New ``ExitOnPrevious***`` methods. .. only:: html Dec. 2019 - Initial Release --------------------------- .. only:: latex .. rubric:: Dec. 2019 - Initial Release * TerminalUI has been extracted from `pyIPCMI `__. * Basic functionality to use a text based application in a terminal window. pyTooling-8.11.0/doc/Packaging.rst000066400000000000000000000252031513317154500167570ustar00rootroot00000000000000.. _PACKAGING: Overview ######## The module :mod:`pyTooling.Packaging` provides helper functions to achieve a *single-source-of-truth* Python package description, where (almost) no information is duplicated. The main idea is to read configuration files, READMEs, and Python source files from ``setup.py``, so it doesn't duplicate information. This allows an easier the maintenance of Python packages. .. #contents:: Table of Contents :depth: 2 .. _PACKAGING/Helper: Helper Functions ################ The following helper functions are used by :func:`~pyTooling.Packaging.DescribePythonPackage`, but these can also be called individually to reuse internal features offered by that package description function. .. _PACKAGING/Helper/loadReadmeFile: loadReadmeFile ************** The function :func:`~pyTooling.Packaging.loadReadmeFile` reads a ``README`` file and guesses the contents MIME type on the file's extension. It returns an instance of :class:`~pyTooling.Packaging.Readme`. This read text can then be used for the package's *long description*. .. topic:: Supported file formats * ``*.txt`` - Plain text * ``*.md`` - `Markdown `__ (further reading: :wiki:`Markdown`) * ``*.rst`` - `ReStructured Text `__ (further reading: :wiki:`ReStructuredText`) .. grid:: 2 .. grid-item:: :columns: 6 .. admonition:: Usage in a ``setup.py`` .. code-block:: Python from pathlib import Path from pyTooling.Packaging import loadReadmeFile readmeFile = Path("README.md") readme = loadReadmeFile(readmeFile) # print(readme.Content) # print(readme.MimeType) .. grid-item:: :columns: 6 .. admonition:: ``README.md`` .. code-block:: Markdown # pyTooling **pyTooling** is a powerful collection of arbitrary useful abstract data models, missing classes, decorators, a new performance boosting meta-class and enhanced exceptions. It also provides lots of helper functions e.g. to ease the handling of package descriptions or to unify multiple existing APIs into a single API. .. _PACKAGING/Helper/loadRequirementsFile: loadRequirementsFile ******************** The function :func:`~pyTooling.Packaging.loadRequirementsFile` recursively reads a ``requirements.txt`` file and extracts all specified dependencies. As a result, a list of requirement strings is returned. .. topic:: Features * Comments are skipped. * Recursive references are followed. * Special dependency entries like Git repository references are translates to match the syntax expected by setuptools. .. warning:: The returned list might contain duplicates, which should be removed before further processing. This can be achieve by converting the result to a :class:`set` and back to a :class:`list`. .. code-block:: Python requirements = list(set(loadRequirementsFile(requirementsFile))) .. grid:: 2 .. grid-item:: :columns: 6 .. admonition:: Usage in a ``setup.py`` .. code-block:: Python from pathlib import Path from pyTooling.Packaging import loadRequirementsFile requirementsFile = Path("doc/requirements.txt") requirements = loadRequirementsFile(requirementsFile) # for req in requirements: # print(req) .. grid-item:: :columns: 6 .. admonition:: ``requirements.txt`` .. code-block:: -r ../requirements.txt Sphinx ~= 8.2 docutils <= 0.21.0 sphinx_rtd_theme ~= 3.0 .. _PACKAGING/Helper/extractVersionInformation: extractVersionInformation ************************* The function :func:`~pyTooling.Packaging.extractVersionInformation` extracts version information from a Python source file (module). Usually these module variables are defined in a ``__init__.py`` file. .. rubric:: Supported fields * Author name (``__author__``) * Author email address (``__email__``) * Copyright information (``__copyright_``) * License name (``__license__``) * Version number (``__version__``) * Keywords (``__keywords__``) The function returns an instance of :class:`~pyTooling.Packaging.VersionInformation`, which offers the gathered information as properties. .. grid:: 2 .. grid-item:: :columns: 6 .. admonition:: Usage in ``setup.py`` .. code-block:: python from setuptools import setup from pyTooling.Packaging import extractVersionInformation file = Path("./pyTooling/Common/__init__.py") versionInfo = extractVersionInformation(file) setup( # ... version=versionInformation.Version, author=versionInformation.Author, author_email=versionInformation.Email, keywords=versionInformation.Keywords, # ... ) .. grid-item:: :columns: 6 .. admonition:: ``__init__.py`` .. code-block:: python __author__ = "Patrick Lehmann" __email__ = "Paebbels@gmail.com" __copyright__ = "2017-2026, Patrick Lehmann" __license__ = "Apache License, Version 2.0" __version__ = "1.10.1" __keywords__ = ["decorators", "meta classes", "exceptions", "platform", "versioning"] .. _PACKAGING/Descriptions: PackageDescriptions ################### .. _PACKAGING/Descriptions/Python: DescribePythonPackage ********************* :func:`~pyTooling.Packaging.DescribePythonPackage` is a helper function to describe a Python package. The result is a dictionary that can be handed over to :func:`setuptools.setup`. Some information will be gathered implicitly from well-known files (e.g. ``README.md``, ``requirements.txt``, ``__init__.py``). Handling of namespace packages ============================== If parameter ``packageName`` contains a dot, a namespace package is assumed. Then :func:`setuptools.find_namespace_packages` is used to discover package files. |br| Otherwise, the package is considered a normal package and :func:`setuptools.find_packages` is used. In both cases, the following packages (directories) are excluded from search: * ``build``, ``build.*`` * ``dist``, ``dist.*`` * ``doc``, ``doc.*`` * ``tests``, ``tests.*`` Handling of minimal Python version ================================== The minimal required Python version is selected from parameter ``pythonVersions``. Handling of dunder variables ============================ A Python source file specified by parameter ``sourceFileWithVersion`` will be analyzed with Pythons parser and the resulting AST will be searched for the following dunder variables: * ``__author__``: :class:`str` * ``__copyright__``: :class:`str` * ``__email__``: :class:`str` * ``__keywords__``: :class:`typing.Iterable`[:class:`str`] * ``__license__``: :class:`str` * ``__version__``: :class:`str` The gathered information be used to add further mappings in the result dictionary. Handling of package classifiers =============================== To reduce redundantly provided parameters to this function (e.g. supported ``pythonVersions``), only additional classifiers should be provided via parameter ``classifiers``. The supported Python versions will be implicitly converted to package classifiers, so no need to specify them in parameter ``classifiers``. The following classifiers are implicitly handled: license The license specified by parameter ``license`` is translated into a classifier. |br| See also :meth:`pyTooling.Licensing.License.PythonClassifier` Python versions Always add ``Programming Language :: Python :: 3 :: Only``. |br| For each value in ``pythonVersions``, one ``Programming Language :: Python :: Major.Minor`` is added. Development status The development status specified by parameter ``developmentStatus`` is translated to a classifier and added. .. seealso:: `Python package classifiers `__ Handling of extra requirements ============================== If additional requirement files are provided, e.g. requirements to build the documentation, then *extra* requirements are defined. These can be installed via ``pip install packageName[extraName]``. If so, an extra called ``all`` is added, so developers can install all dependencies needed for package development. ``doc`` If parameter ``documentationRequirementsFile`` is present, an extra requirements called ``doc`` will be defined. ``test`` If parameter ``unittestRequirementsFile`` is present, an extra requirements called ``test`` will be defined. ``build`` If parameter ``packagingRequirementsFile`` is present, an extra requirements called ``build`` will be defined. User-defined If parameter ``additionalRequirements`` is present, an extra requirements for every mapping entry in the dictionary will be added. ``all`` If any of the above was added, an additional extra requirement called ``all`` will be added, summarizing all extra requirements. Handling of keywords ==================== If parameter ``keywords`` is not specified, the dunder variable ``__keywords__`` from ``sourceFileWithVersion`` will be used. Otherwise, the content of the parameter, if not None or empty. .. _PACKAGING/Descriptions/GitHub: DescribePythonPackageHostedOnGitHub *********************************** :func:`~pyTooling.Packaging.DescribePythonPackageHostedOnGitHub` is a helper function to describe a Python package when the source code is hosted on GitHub. This is a wrapper for :func:`~pyTooling.Packaging.DescribePythonPackage`, because some parameters can be simplified by knowing the GitHub namespace and repository name: issue tracker URL, source code URL, ... .. todo:: normal packages ``PackageName`` namespace package root package ``NamespacePackage.*`` namespace package sub package ``NamespacePackage.PackageName`` deriving URLs .. admonition:: Usage in ``setup.py`` .. code-block:: Python from setuptools import setup from pathlib import Path from pyTooling.Packaging import DescribePythonPackageHostedOnGitHub packageName = "pyTooling.Packaging" setup( **DescribePythonPackageHostedOnGitHub( packageName=packageName, description="A set of helper functions to describe a Python package for setuptools.", gitHubNamespace="pyTooling", keywords="Python3 setuptools package wheel installation", sourceFileWithVersion=Path(f"{packageName.replace('.', '/')}/__init__.py"), developmentStatus="beta", pythonVersions=("3.8", "3.9", "3.10") ) ) pyTooling-8.11.0/doc/TODO.rst000066400000000000000000000000331513317154500156320ustar00rootroot00000000000000TODOs ##### .. todolist:: pyTooling-8.11.0/doc/Terminal/000077500000000000000000000000001513317154500161125ustar00rootroot00000000000000pyTooling-8.11.0/doc/Terminal/index.rst000066400000000000000000000030431513317154500177530ustar00rootroot00000000000000.. _TERM: Terminal ######## A set of helpers to implement a text user interface (TUI) in a terminal. .. _TERM/Terminal: Terminal ******** .. _TERM/LineTerminal: LineTerminal ************ Introduction ************ This package offers a :class:`pyTooling.TerminalUI.LineTerminal` implementation, derived from a basic :class:`pyTooling.TerminalUI.Terminal` class. It eases the creation of simple terminal/console applications. It includes colored outputs based on `colorama`. List of base-classes ******************** * :class:`pyTooling.TerminalUI.Terminal` * :class:`pyTooling.TerminalUI.LineTerminal` Example ******* .. code-block:: Python from pyTooling.TerminalUI import LineTerminal class Application(LineTerminal): def __init__(self) -> None: super().__init__(verbose=True, debug=True, quiet=False) def run(self): self.WriteQuiet("This is a quiet message.") self.WriteNormal("This is a normal message.") self.WriteInfo("This is an info message.") self.WriteDebug("This is a debug message.") self.WriteWarning("This is a warning message.") self.WriteError("This is an error message.") self.WriteFatal("This is a fatal message.") # entry point if __name__ == "__main__": Application.CheckPythonVersion((3,6,0)) app = Application() app.run() app.Exit() Line #### ``Line`` represents a single line in a line-based terminal application. If a line is visible, depends on the :class:`~pyTooling.TerminalUI.Severity`-level of a ``Line`` object. pyTooling-8.11.0/doc/Tutorials/000077500000000000000000000000001513317154500163255ustar00rootroot00000000000000pyTooling-8.11.0/doc/Tutorials/Attributes.rst000066400000000000000000000001151513317154500212020ustar00rootroot00000000000000Attributes ########## .. todo:: Attributes::Tutorial:: Needs to be written. pyTooling-8.11.0/doc/Tutorials/CLIAbstraction.rst000066400000000000000000000004641513317154500216640ustar00rootroot00000000000000CLI Abstraction ############### .. todo:: CLIAbstraction::Tutorial:: Needs to be written. * How to select Program vs. Executable. * How to define a first argument as a nested class derived from existing classes. * How to choose from existing argument classes. * Single value vs. Tuple vs. List pyTooling-8.11.0/doc/Tutorials/Decorators.rst000066400000000000000000000222031513317154500211630ustar00rootroot00000000000000Decorators ########## .. #contents:: Table of Contents :local: :depth: 2 Decorators can be applied to classes or functions/methods. A decorator is a callable, so a function or a class implementing ``__call__``. Decorator can accept parameters, when a decorator factory returns a specific decorator. The decorator syntax of Python is syntactic sugar for a function call. See also :ref:`decorators offered by pyTooling `. .. hint:: The predefined :func:`~functools.wraps` decorator should be used when creating wrapping or replacing decorators, so the name and doc-string of the callable is preserved and decorators can be chained. +-------------------------------------+---------------------------------------------------+-----------------------------------------------+ | Function-based without Parameter | Function-based with Parameter(s) | Class-based with Parameter(s) | +=====================================+===================================================+===============================================+ | .. code-block:: Python | .. code-block:: Python | .. code-block:: Python | | | | | | from functools import wraps | from functools import wraps | from functools import wraps | | | | | | F = TypeVar("F", Callable) | F = TypeVar("F", Callable) | F = TypeVar("F", Callable) | | | | | | def decorator(func: F) -> F: | def decorator_factory(param: int) -> Callable: | class decoratorclass: | | @wraps(func) | def specific_decorator(func: F) -> F: | _param: int | | def wrapper(*args, **kwargs): | @wraps(func) | | | return func(*args, **kwargs) | def wrapper(*args, **kwargs): | def __init__(self, param: int) -> None: | | | kwargs["param"] = param | self._param = param | | return wrapper | return func(*args, **kwargs) | | | | | def __call__(self, func: F) -> F: | | | return wrapper | @wraps(func) | | | return specific_Decorator | def wrapper(*args, **kwargs): | | | | kwargs["param"] = self._param | | | | return func(*args, **kwargs) | | | | | | # | # | return wrapper | | | | | +-------------------------------------+---------------------------------------------------+-----------------------------------------------+ | .. code-block:: Python | .. code-block:: Python | .. code-block:: Python | | | | | | @decorator | @decorator_factory(10) | @decoratorclass(10) | | def foo(param: int) -> bool: | def foo(param: int) -> bool: | def foo(param: int) -> bool: | | pass | pass | pass | +-------------------------------------+---------------------------------------------------+-----------------------------------------------+ | .. code-block:: Python | .. code-block:: Python | .. code-block:: Python | | | | | | def foo(param: int) -> bool: | def foo(param: int) -> bool: | def foo(param: int) -> bool: | | pass | pass | pass | | | | | | foo = decorator(foo) | foo = decorator(10)(foo) | foo = decoratorclass(10)(foo) | +-------------------------------------+---------------------------------------------------+-----------------------------------------------+ Usecase ******* Modifying Decorator =================== A modifying decorator returns the original, but modified language item. Existing fields might be modified or new fields might be added to the language item. It supports classes, functions and methods. .. code-block:: Python F = TypeVar("F", Callable) def decorator(func: F) -> F: func.__field__ = ... return func @decorator def function(param: int) -> bool: pass class C: @decorator def method(self, param: int) -> bool: pass .. seealso:: The predefined :func:`~functools.wraps` decorator is a modifying decorator because it copies the ``__name__`` and ``__doc__`` fields from the original callable to the decorated callable. Replacing Decorator =================== A replacing decorator replaces the original language item by a new language item. The new item might have a similar or completely different behavior as the original item. It supports classes, functions and methods. .. code-block:: Python F = TypeVar("F", Callable) def decorator(func: F) -> F: def replacement(*args, **kwargs): pass return replacement @decorator def function(param: int) -> bool: pass class C: @decorator def method(self, param: int) -> bool: pass .. seealso:: The predefined :func:`property` decorator is a replacing decorator because it replaces the method with a descriptor implementing *getter* for a read-only property. It's a special cases, because it's also a wrapping decorator as the behavior of the original method is the behavior of the getter. Wrapping Decorator ================== .. todo:: TUTORIAL::Wrapping decorator .. code-block:: Python F = TypeVar("F", Callable) def decorator(func: F) -> F: def wrapper(*args, **kwargs): # ... return func(*args, **kwargs) return replacement @decorator def function(param: int) -> bool: pass class C: @decorator def method(self, param: int) -> bool: pass Without Parameters ****************** Function-based without Parameters ================================= .. todo:: TUTORIAL::Function-based without parameters - write a tutorial .. code-block:: Python F = TypeVar("F", Callable) def decorator(func: F) -> F: def wrapper(*args, **kwargs): # ... return func(*args, **kwargs) return replacement With Parameters *************** Function-based with Parameters ============================== .. todo:: TUTORIAL::Function-based with parameters - write a tutorial .. code-block:: Python F = TypeVar("F", Callable) def decorator_factory(param: int) -> Callable: def decorator(func: F) -> F: def wrapper(*args, **kwargs): # ... return func(*args, **kwargs) return replacement return decorator Class-based with Parameters =========================== A decorator accepting parameters can also be implemented with a class providing ``__call__``, so it's a callable. .. todo:: TUTORIAL::Class-based - write a tutorial .. code-block:: Python from functools import wraps F = TypeVar("F", Callable) class decoratorclass: _param: int def __init__(self, param: int) -> None: self._param = param def __call__(self, func: F) -> F: @wraps(func) def wrapper(*args, **kwargs): kwargs["param"] = self._param return func(*args, **kwargs) return wrapper pyTooling-8.11.0/doc/Tutorials/ExceptionHierarchy.rst000066400000000000000000000002361513317154500226550ustar00rootroot00000000000000.. _ExceptionHierarchies: Exception Hierarchies ##################### .. todo:: Write this tutorial. **Advantages:** * easy exception filtering pyTooling-8.11.0/doc/Tutorials/MetaClasses.rst000066400000000000000000000013231513317154500212620ustar00rootroot00000000000000Meta-Classes ############ .. todo:: Write this tutorial A Python meta class is a class used to construct instances of other classes. Python has one default meta class called :class:`type`. It's possible to write new meta classes from scratch or to derive subclasses from :class:`type`. Meta classes are used by passing a named parameter to a class definition in addition to a list of classes for inheritance. .. code-block:: Python class Foo(Bar, metaclass=type): pass .. seealso:: * https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/ * https://www.pythontutorial.net/python-oop/python-__new__/ * https://stackoverflow.com/questions/76468665/why-does-object-new-accept-parameters pyTooling-8.11.0/doc/Tutorials/TerminalApplication.rst000066400000000000000000000001521513317154500230140ustar00rootroot00000000000000Terminal Application #################### .. todo:: TerminalApplication::Tutorial:: Needs to be written. pyTooling-8.11.0/doc/Tutorials/index.rst000066400000000000000000000004141513317154500201650ustar00rootroot00000000000000.. _Tutorials: Tutorials ######### .. admonition:: Exception Hierarchies Write how to plan and implement an exception hierarchy. .. toctree:: :hidden: Attributes CLIAbstraction Decorators ExceptionHierarchy MetaClasses TerminalApplication pyTooling-8.11.0/doc/Warning/000077500000000000000000000000001513317154500157445ustar00rootroot00000000000000pyTooling-8.11.0/doc/Warning/index.rst000066400000000000000000000030071513317154500176050ustar00rootroot00000000000000.. _WARNING: Warnings ######## .. grid:: 2 .. grid-item:: :columns: 6 A warning can be raised similar to an exception, but it doesn't interrupt execution at the position where it was raised. The warning travels upwards the call-stack until it's handled by a :class:`~pyTooling.Warning.WarningCollector` similar to a `try .. except` statement. If a warning isn't handled within the call-stack, it raises an exception. A warning is raised by Calling the class-method :meth:`WarningCollector.Raise `. This function expects a single parameter: an instance of :class:`Warning`. To handle a raised warning, a `with`-statement is used to collect raised warnings. Usually, a list is handed over to a :class:`~pyTooling.Warning.WarningCollector` context. .. grid-item:: :columns: 6 .. code-block:: Python from pyTooling.Warning import WarningCollector class ClassA: def methA_RaiseException(self) -> None: WarningCollector.Raise(Warning("Warning from ClassA.methA_RaiseException")) .. code-block:: Python from pyTooling.Warning import WarningCollector class Caller: def operation(self) -> None: warnings = [] a = ClassA() with WarningCollector(warnings) as warning: a.methA_RaiseException() print("Warnings:) for warning in warnings: print(f" {warning}") pyTooling-8.11.0/doc/_static/000077500000000000000000000000001513317154500157655ustar00rootroot00000000000000pyTooling-8.11.0/doc/_static/css/000077500000000000000000000000001513317154500165555ustar00rootroot00000000000000pyTooling-8.11.0/doc/_static/css/override.css000066400000000000000000000042011513317154500211030ustar00rootroot00000000000000/* theme overrides */ .rst-content h1, .rst-content h2 { margin-top: 24px; margin-bottom: 6px; text-decoration: underline; } .rst-content h3, .rst-content h4, .rst-content h5, .rst-content h6 { margin-top: 12px; margin-bottom: 6px; } .rst-content p { margin-bottom: 6px } /* general overrides */ html { font-size: 15px; } footer { font-size: 95%; text-align: center } footer p { margin-bottom: 0px /* 12px */; font-size: 95% } section > p, .section p, .simple li { text-align: justify } .rst-content .topic-title { font-size: larger; font-weight: 700; text-decoration: underline; margin-top: 18px; margin-bottom: 6px; } .rst-content p.rubric { text-decoration: underline; font-weight: 700; margin-top: 18px; margin-bottom: 16px; } /* wyrm overrides */ .wy-menu-vertical header, .wy-menu-vertical p.caption { color: #9b9b9b /* #55a5d9 */; padding: 0 0.809em /* 0 1.618em */; margin: 6px 0 0 0 /* 12px 0 0 */; border-top: 1px solid #9b9b9b; } .wy-side-nav-search { margin-bottom: 0 /* .809em */; background-color: #333333 /* #2980b9 */; /* BTD: */ /*color: #fcfcfc*/ } .wy-side-nav-search input[type=text] { border-radius: 0px /* 50px */; } .wy-side-nav-search .wy-dropdown > a, .wy-side-nav-search > a { /* BTD: */ /*color: #fcfcfc;*/ margin-bottom: 0.404em /* .809em */; } .wy-side-nav-search > div.version { margin: 0 0 6px 0; /* BTD: */ /*margin-top: -.4045em;*/ } .wy-nav .wy-menu-vertical a:hover { background-color: #333333 /* #2980b9 */; } .wy-nav-content { max-width: 1600px /* 800px */ ; } .wy-nav-top { background: #333333 /* #2980b9 */; } /* Sphinx Design */ .sd-tab-set { margin: 0 } .sd-tab-set > label { padding-top: .5em; padding-right: 1em; padding-bottom: .5em; padding-left: 1em } .sd-container-fluid { padding-left: 0; padding-right: 0; } .sd-container-fluid > .sd-row > .sd-col > p.rubric { margin-bottom: 6px; } .sd-container-fluid > .sd-row > .sd-col > ul.simple { margin-bottom: 0px; } html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property { display: inline; /* inline-block; */ } pyTooling-8.11.0/doc/_static/icon.png000066400000000000000000001255111513317154500174300ustar00rootroot00000000000000PNG  IHDRFl]zTXtRaw profile type exifxڭu9maEy0Z3A$%HTK"@ uTjn9[Pl?XzFwy=@M.zAw֭k ^q^ko{,c%^ \_)]xR9~5Uz= suߍ9Bp۾.~z~lW~=ۣܟ8g|&~.Um/u;9Oďnt)F}o~dD} -P$s{ߦqʝcw\̾2|O>\s/_Fw^1M7IN7)?//$"LTIV)fRBݤbJ)jjcN9璅SK*RK+kZkdZnZܴε:;/ ?ˆ#<ʨ>)gyYg}`V^eVnSJ;.N8O9dՏYg~5ʚҸ3k\%$)gdGGƋ2@A{VW3|0!$,2Fv>#w?3m y_e(u?2gw/p.$ dNq6|-qv=>vK5.kӴdn@+rV ( N\ax}>h߆?\6?g<mj.Thcxyiuv|⚣MgFwu6Ȕiih&ld_ӪeSa0ώU= XTn4tsC≳աq,ZMv[O9|K="> y" O7pʍy6uovBujYmĤ="Ҏg 40tbm(s`(7FV:W9]smG{P 6r :$;Z UR~Һg짵W+d7mN(8LR9Lg BQ@Q03YE1@{p43Ϝ_ X_jYkfǚo6mƚ {z6c;7lv:->>USE jet*]3X[>5&o4hK'`:be*;6+I-OeNt(k\+|\1"茝<֞k.\*Or/M LcR{G'L=ÊoI6Z 9dvmgԔ߳9 ]{ºP`O+qi-M)(EjL@4JٳCpns3׍g۵|fH_?vFUr\YDN.M.քwbꈧsЍdDŽ$+xPPvĐ{DT u њB9u}!ӽe;n ?Wn+cj?!C (WrqO]pGz̚zXw+U!,J(Af*ƊLHe2 Cȣ`4g;qT* EQAᶢ>&_tZ R'A!kȣ_~T\fFKWZoy6# f]M 5e⠽F}R#.-5VF&5bW]Q|nHg ]I g̹S+C3Lw75ĥTǂ{Wn&G}I?^{qY(F;6&KR$;۞t]sṬʜUfF!JtrJ7ItmۣoI5&1&hQ 8\(qE1vP\d4`h@&Mma6.S7T! Фif;'4.7M594qjaEW9K̙pr՝ Vzbn-l~5>ՙuN~MrY9%6q)-l=qí팙j>BSDm'vTDKBPoÐqk6=7)Iı,D'Ql3Jl'p%B/S|M6fk@PܙTc 9 љF)D X@݈Y) #!׃8%e h@~!põof.u4`\dZ*ŭA$ۏ8PRTZw%cѬnGUT7i3T'cE;R4 o2FAډ 9YW t鶀9W|.iPPxZ꘵FIC:Y1P%NXԟVvITE/y7Uw?Вw*:( I' G͍E}@y"yAl*;Tu-bvr)KLbL- S3ܧB>a;)*!K:cN &$'hБ5о XW* Fw!FZjyc2N$7w0a3VbvTA&ff4jdt%aHCRh}@`tP!\P@ĝ K#+. o7@a'OgP#B" [M< 9`0/4hedzG,PliZ^,QWn<(PEriPIP`i[7ye%fTiӽ)HRHcרյ RФ.7SK;*kڲ>aJ}f& ;dAcó޽Y4!9-|Af"kg63$^)&˃iC: iAQGMh"qI)g.r:k&>ݤ)b׺U9έ(߁5J߅oVS,mH;b%?R-ZFQip Q4_@Q?p-K ;ݍAڽl(ǧK"_8i]2 & Er?vjwL%JQ0ܸ%z]|Ǡ[%RԲtv>WDGE5kC+C%<Ƚl~H'Åx h CIEs(lASG`vR-  upk i-:)$GN! 6>gid{qoEcᠷR{pZ`T4S΁VQJ_` mJ a}~d'Ok XpIlbE %E+.s"" Nsd&*)HC-z"1}7pjqH:u%Z⚒;W$o9 ))PU6($ (2q@׾4ۉ%l/V*3مsP a>@%@0:'%ɊZAJՕ&4Z)X MH ;iRT:jM S4 ~Y{mnbRo{;\G$Y:@ƁJxE-T}RٴEkSBFTkm(H%G)ew-UZA)!5Jc; 7x,@N OCpMCwY&d`IHZ^PMR5џ* 0O;ashB~P0ZZni6% /hdLuC!L)\(pu#k6=NjZ91?z!U¤u`RSҮh AB"z3Ь-W8D  oQُ RF؆+}L{ȩU/C<] ץ&h~9)Tml&~s#Ç,qڂjԧ0>3\? SQ!TK3)n8Mt.l]ǁ;:NI׊SЩZ~WMw!?#UWJWijt]TqX5-ЇH1ĠDpu~a?gZDw. P6ĊCpmKyj95IO9Ԫ@xƙ8y"VTWpՒ{E 9les+1 aӂFkIH7ڠ!B0/R]^2$hqD:p] aUZ L \H? B & ,AI@wňZAΞ«ke7Z< 's_Y ~WCHr\YFMǠc!ȇ3 Ɯ-YsC3'KNv9:+7u>;D.gUvkEXQH[Bh% Ļ(ayd`D-Ӣ漌nLu]!= )+ 9D@(Zע4%rHA4bx{Q|8=ma6H74 VcFmil6ZԣHV㴽آxX$['A{g*цM0'H:K`C ޅ$$ !赗sBSD)w @D1KGwyW쏒ӳu/IkWi=><2YN-o.ƃ|MN^-ʝ qy:z:quW8kuNk# -k\B#ݮ6 &m\r"昐bpʽR3m߻'.U-9\?%סmvV b@%C#yai@HeJ/qtun ۃgC>e]2U;RH 5 "\uikY ]}Je'tFk@Ѷ6sA52JA:SX)K '1-`RrLV;)ύ!_p#1<ŀDXRmPg8WgDHx MzS=ן-og NZ6 KY2V MCxCPV2nHw -K|twi/'~78Sv kL$/8`)Xuu C/bϭJ\CCm@!a4K']Gec6n.~POXtt 2FyP >Ls6;xlZQ(/α BCO}t.?mm/|>Mo٦GhSxOnͤhmwKaW{?>$'dv$r$ߧ4Q]h%PGZ+bavZb:%AqC~[g~.z@|_'QFU mw,*:a/O>RWKCyQ_m9^R0cvv0$RdkӟDGQQOGl;*ڼ_ZJz΂-V)]:q($c'#_cIm+3!=$`k}&bqƆS]rB7ywzs\'MF7o oh\3~t񷿛?9kкΑWAofԡ z".6ŀ*m/w?a" yY!B%iJ"{lu3UBhO*xӌ"E8Nx9#]21r%+2N+pY%~E,;y垅O (QGf(P bstrm=юd־O&rvO^!}<#5VI`NQRZ ߵ}]xBL'r ?߼=@=RQ'}4Ă{E42V].oyx;2TC4w<_c(?c`ƯP9"MXYUi~*M$~~,"CVΰ#CFGS Na&H`b/|?">97iCCPICC profilex}=HPOS"- !CdATQX ЪK!Iqq\ ,V\uupG''E)" q=Vf8jM%BqE ‡ˆ".1SO{ꦺK,?+LDY:s'tAG.q8,̈GJ=̪J8599L&i%I\.?~\{ر-G4˗/ĉZ^YQ\ԤFFF }V\Ғ~i500:MScd߯WwAj۷3N8o|ıqm߾ #WF_|Ej5}{511e56GVSO=o:|z{{}H<5??lsN/\ܐ`}VJE۷o$eXkN8!co߮J"I|9'NhVS&5>>k&\cua=䓚zC+[@0 +H["[j;v:OhllLtI}+_ʊ>x5wHQirrR333D1F\N :vVWW%IΝ&&&Z6bMUih\2HE7%X>`_~'t1?/|fffZ -ꫯqed\azk14Mo{+ƴ_~Y I~Xccc-քk_=j.\O|zOa^ګ7=<'OԹs433/| m!ΝS6UEt__ݔv {9箶8箫o{W^\?~XRƿWrVr$\-cDZΝ;GJ W^,irrT|lVrVr$\র֪\.U,z)~7$\ʋWF(c=ǏJklcss @w}0TPСCtaq`wZyRhvvZ+9 xWZG+;З%i}}]aT*s6 ȑ#?cʋﶒȈFFFkz1K###Z^^ֱcͫ$=K]I9Yl5@ \kp p o09*`K8v\p 5@@ \ \kp p 5@@ \ \kp p 5@@ \ \kp p 5@@ \kp p 5@@ \kp p 5@@ \kp p 5@J64vCґ,iGF53Uك0ܙDI=ÑTǟ[THc Z9Nf5y[oN7甬&J #I>IIlrb;U-&;)j`"V*mxyoF^PI=Oq 0׽$$/KIK B4IU_KeUYkK׍Mc:Ḙ?9Rǎ9Gnw&4GF %#x)̆ "02Vs>7W^j$Umy]jV4v]_WlHkݏ(J=R5* g9J2l{/I^ijS)i?tT]. ʷȟec \6WЗip:\ѨЛQy8/c F֙}T_7ZsZkhq^|jM/5>\B#lAwCefU+_vrF^ [c(c#qVRE#U"5^RC,jĊw^'n@` `3)ߛԭeŅH9W[y"lɅV.bJU o/itWI\ڊYK<Si!gTs 3NqνO.FA%PXTy8FMݷ/^Śyk \@ױ@{2:P]B_V^F֤j Hz" fPuK:-jTB \V4E 7+WʘTZO1^A${#yB.V5s^'[Tm~]jU@-4j`OIc*^Te0V\dM"啫_ՌwUԨ5tWϩ^] d \@g VSw5uي*9QsԵ EdقUFžHќ.Xӓ p 8m:۫#qc//cJ}.48]xqI_ZSζvrVXccb[hq.t{_W}l`+bp&F3U^Rzye kT@&)o\њ+l'!\Mdi@N) l9O?u۹2a6~=S: Z9 pg5~/#B7rXó4o1]@azu/R`IIFJ펪zWOz^2@xn۟=?3) [FOWN?@Q !zn)ύj#Vo%XE4-SBkhΜnԘ&o):DlQxUczlTќ?z5`SS@*29+kRr]<׏yA?W%I&0 p n׳uOrf6JbDYy"/L y&kwM{P&#7զ0]Y`rFY9}:Na5ZdC 0'Tw9#/g*>2*w[A=65)N xE72:q5S}wNSp x+VcÚ>XbѝR}5w~'тӶ{c˚۔4)@dBPn*}N`t?Ux/'LK&"İn}tLٜ1#B畮s˔fb|bT>4"k/)Yo3liJ׮0gǴ!56 xbBw>9D15tGTAhtcG(@ώh= -n'`knvp@`U"Ȱ ~O{THt{!}} ` RT`<վGuP p /7ȎVxnydX Mi|_YAH-p2- Sp ~( `1[:]Ŵv30C+$_g$vaȈ2/C96K5 \@ˍE:ؤTh/Φ y S }gsZD{2 Tӷt'iЦ*o);v׫ekh3@?5A#"qU;-@v9 Fwt #:nZΟ-9j=LWG. λTP#+ J_kF*/1Y3N~O;c*Z :Ӈyf>nݟr Yc (YQP )@k)p DV4zH15XrD5cz"mP *C!&V-%J15>ҫ%P lDzzr#@h*[b[1^S9Mo&*S*3Cbi궒 \@+e d2FwUC \3ѾG?]F"*e \&b_FHZ[TGs>A!5lYhAcFvV(@3o)+wȈbff \&QT&ۜmP{gQp ?j@YrH1Еh`2ĀÑ,g:t\O>ɋއ yߐbQxD!5wSʗ,3L[~Bk~($E@wSp NAl,:Ap *\@Ci^p&2 N;3 \>Ȓ7^lBkv#{+:ZB \5(YFU\@w`QugASp q"Ii" \;@ܖW0B[W?dH{rʕY8xQ#էc \ N.2>;kkt3~RPpwTd,-!;J@IB%5MXk \0XWe4#skxh5;lSpMx|e55:m,:$Vyh@ձ"Q7TL 6#:;\Y+Tqkc|O0t\W0^Ade \q2ڌvFQ.p$fLV2bɼ)H{oRo=!-=ݞ&M-)r*h 5ͱR'FJyQi(0n>n&I;*gu% t=g(&\4+uz-%jzkj%Ֆ|*R@ BP w$yb)S~b7,=U$˓n|e4zY/|{^_Y֩gT_Mv_{Gk:%)M]i>VQw# \$5 +PIC?;/7[;Ac1ՙ43K:&4|9]"75@2op lqE tXӇ)2䏖{oGtN#zOkRB0Fa>RT"k+W TŔo7BC/=3oۓZ~e%S}V'-% DͭRp N׼=/VYyhHRD[*U)S%9SG")xCn%ν7o#pQ m+ɅV%5I"X<}{yI'u7uT'ȮF(@05wzUiWn'8D BL@wG?gdVR~aE/}k}o:l>y]>SkJk<"<[hu~].jM_U2P!k0ְHxox9ѥ7b_\[e+J> t\OF.4 VZиůcU^(ap N602ZbN=;nȵ s[љ(JF> \ZA*ʅ2\|MsGo<~c=ׅP^BZ!IRϋ@wTkW`Ӄ'm` l%E[\^tD\FVV\ҥ:A&r6- \fa)(%ZFC$g%޺k͔O+@V/׵xa0TVOŐӢpz3p ]0aKF2jzU[p Ņ@J$A VsQ6D.$ZYhMdM @͒++4;򾗘Zd}5rBu+IzBʼn7EIjz4@Z{klVM>ҼuRk].YG(hIdL[!2|mӂhmm*k,3[k] p€N-7a*ʐZ=O5hu!zp[qۮ:St\Zqzkp #A2}D v{M,vi͔ @2@ŞB@R ׵TˍκX檜tkݫQKs-/cѰ]佑OM3{ګ5V[JXgVke|Vo4fKncvquY6I8z5n*,܊*Mۧiw0~cxɴJ5[Bl5@͘P^VֵOܺtӵ9ҍ˫1򍵉IdWըd_䓶g8RH7'(MW\KIꔤk]m})=- LSf0j$JwP|RFDtlũHuk7rm/&ZtP ;m(pp,k,˜SXKL3Q׍q3ziy:@/5vV\L*u6o逩MdKS\8k`S@'r[jk1Fې1#F6Q6|E t$V-o$ڪae!R/kKw­3KQYHUucN 9StM7@ת-Z]lHp>a+_vd"P)M,/5k]}rFΙG U[S^p y\9Zʫ],Z^`:gdM1C 7@FkduhQe@\Z˫ ].xCYe 2qF6/:_?˚$OSTz[TRtHVZшbl͙.h8kFVD4JRl IDAT˜7,@de:p2(G(*ӓS #22HJ`^>JT>֗VrnI+gT_SZOTT;tGF.d5q($PJR1xٷR7S[8#xgb'RR*V%8E%K Uai})ʙNڥD.2 V5bb6? \pVQOV.v rETsr@Z 5.*̅A +eNo)#2U5&U_]WRk'6/մz~I˧vaEՆά(]jQL)W֢to>Ivm9uTalIr;Ӈ(\6\\ʅF; 2Vi#UfTa)Sd \ 笒F4iGՖjU-' 汱(ixW.7T[Hv1 $%p.žX6b).dC+ X9ɾü9o ^_ F |'u՗T_^WROZ>K?:sKJ5.+!lo뫩i[r~5_UǿvjN;5GӆWNk9=ŒUy0jdwUIqV2)]`dlisIߜ+iHF{\k}5U\ns0:I ϯ0hB7p5:b\1qVPN]dCHEYk_ ŌB?z5fU֯BdҊ_X+]KrTzzגs2&;Wr68ik4_^NXj$ɪw HAhrs*w% 3K*+dQJ Gk -ԕ^ZsGtS5TZ>ېo 6D;4lA6pTUi|h&Hw .o5 A-Wc\k|<$57L>m!TlM6i\U0oU̪:Qu)ԗQKb8(2VJTڂ8a]ݧ[~uFֽH{Kij^-[URO%xn]I#њkV/6uO&\mNȕBTLo^N(*2ּuKV˖aeBTꓼW|涟Қ_\RvGK6&p5HեT+Kd6}kU9o sir{˪e-* *fT睂*8;6p{59k^=3ū_e0+.Ź5媡._"| :5n WUVQn9ݳ+ٍ-3lЩgzf499hȜj'ncIݫ^Kwx}BUb50foԻf[M:PC3*48W/Vy(8Bۜʤ2uo6WiN':!_m2V:˚W/􏗴zu13 -gNѼ➬U5`vn]o(My~Z~e^uFL}-JC}Wghn٩_yƝr#VN'K*̩ЗQur)}#+#[3rh6}OZE_ӥӫN=ϯF ڄklz3uHcwoSiG&o 9J:wGtZ>6uBv(TʽV:a(Mm[=^_]JT[M˳}69ɚDnfQF}rkׇU U* u6{+9zc m/i̪wpnMg_u5x':ko@Fӟ٫K؜@;5YuF n8k+L+4@ZZ4ʣ8k߲E k^NU*Sp22+;I;YPe$W4zUOV\׸P=ծ/ݡ{ 1O7^ԫ"WcHkFkHݭgV%X X'>PMKNq)t +Ny[|Z]$8*zFrZߓ̑ .j욎}gA^\Xkުae{ ܸKőv=qPSב{m( 3mt$cj͑93\YfoOF#V~pNƨ9JM޼4rU9MQ}-ӫ*dtG<"\׎nUëse⦦l)W};4tpB?/O)].,Uqn]jsrzMrY6fkg9xTΨ2i|I*ضz1v+S9Si}p|IJgEY'՗ \wLiCb,m=ٸQ7zp|NߒF˴l2m5+Wm5Up݂7 ^Fo+hFͨw(h Uhaf4ReKH; ꪞ Z9˪n9צ@*j[U4秶%e9s´PxĴ褔ʚM_T U+pw$T#qJs=yI{,:7Ul}I n+2LMՑ~ghyz [Ԗxgd*3* _{كTg@^eyUtGu/^0:v8>Zܓ4<h[776g-mwVaY'19ݶ5F&2@.Z?D[6 >x\c .fR7]:~/IǍm|Jȫ}p1\j {SoPe؆Bא۟{*:j`U=t댊ezFs,(9v:ugOw߈26P~\Qq)uk/i hV>͕ e?o$m.MǍXEE}ԡUGr䚁 u`+\N5zU7ẃ@U>دDUPa ̖F}GԳm@N?uT{Vr(PZ_M-cYs>#Za,YDZҊ)msEηohւ&W?]`UԎ?(T̪P2_,tK0hᝪLŔ()F+:7sBgV^Z01m!-?iv񶖑~chE{54[R3*iB+ZE@q!'K߾ck:4OX םN[󇔭Z+T.^$)gdhkQ|D*=כlCΥoۧ=2NORjtoUVl=g,@Cnc%Ujc(*f(d@P?Okؒ;%em9B`k5G2&o[1rFi" *2Vxɾ[DY (_ UbV.:!\ު6'd k1[~nw)Yek^V5;m4a&g^>J[zDۊgThN݆bQ 8L1ЅWVtKp{QЀvr^asF>;t~I<# @\E:FZ%m&VRkF16dmٜrX=yYt9ZJvB.i+yobQ;-0wIRiey^#~7 2ؒ博}sj4F+fl (UxVެ-AT | m+髿r\K(*j瓇4>eָշkHT[ "{5hToI2J}-XerP7JOIY/1)M#¬Ӷ;NQ7V]C>4 jg@~ w1jRV њQF0-WM<%SZ51iK3w 7C~Ugdv5*T}lV&0}+Q*sݢlݜBV릋6Akr7j4xB\'6~tFӏSi" WԶOUPf7k\ ZtfKiCJ njިZ+9mU `اkûU6!XcŕOߢh(KA^ju?[SYU@5d[^7&i;ʹ6M;U*h -9Q)CS[V$W‚vm7NF^Ƥ]R~5o6^rMnCޮnSaHF+1  6OSawH Ufmۍ_ms^͗ I69ohlM ׸j⳻4JbBP>_7v5oLӟڧm}]_$BZ¼vQ[9*m7RMpݖߩv*?Pj dCU5iRVx!E{FurS][!9e ׆k j3|x<#ָ)[Łzwkc{^z$T\JᒆߴN1ڴ- `&cL̀9ܢD+K-*E-֩l[-൲]O*IL"/)sMBz֎oSoNA2vkt_4VzЪ]lv%Z˶Q?؄rh@;?wPٞ|smd#u+9.%|v學0T&r}FܰHyYH7+؄NVW;>{@2#hÀ a.퓚ޮEB%M%nFIbSܴo ڪU `KaOU3E^]D'!Hn'f2VqSXkی|\_N"~MH&>SG(:% )*d3wϨ-LTˋҒ 1Kk(#n6S 33'w)%5z͋]lnL)؄3}t?@ W a(HUF7.k˩un6C6a2?ϕy>?n&\ogF=ob %$i3-\nhinܒ C*L]!@V ؇?ߚuu6'zouk`k @UŽ=o緂\9P7d#@*mykR92H{˺[Of3ؒUm~w哭3g#Zv~iT 1| lBl Wh$+_`ǖַkXUH|Cl0˹DֵOsVqΉ=`h$oȞú:\O?WAah2?SY1nKxɧFc[dl?3=;]=7 ]!.g5x`Lyldb󳵹:_jMs'8ͼ~3 P xL[%i#{)d -QuvP(M["NmՀM>T>хVbj@Еz3;TJv. Q:lTdV2#׭26s~tVIS/Sn;2 =ugh0ɇv*GThBVo݈s5-_e77rkٔ3u_|ٵpmb*Sʵhq #U|jn[Ҵ^:K56ϻQihGYiug G5rxGimy*KIFj W[^jtwE.i擷(E]Uäms]; Irɂ>ޙuEv@#nSaB' I+.m\[z޴{i ]q/7e:sˇkU j-}較C[ta3m6ϵ$o.`k_eW4v8On7A5V߾Y<4`U~~.l˺Tͮij=60ʕCeKlY6?4დ[()*OnSQnm@OE&(.?q@\^ /Yc,+?^_ej+zeõ{B 6nK&(P 4NkkIx/,.6޲fU,/֪m@2ŀ$kۯ-dºί.Q%Z[+*wV\ ư;GճcP2jߺ{\kJ~vts6\ȹ թ7?&V4R5-^Y[2\}zv *_`|s˚?zyŁzw Z'tY-xj$:^N%y,S[$%T' /TӯHEI}e]'WSQFAThN]㋪_hoNFnͅ'j Z*ls`+[<_U]xVfr3;]mc*;Nkug?`-IϾ*8mo߈{d>=$ު2m Ta~u]_3Z^,&lL~}˧Wt  ʼnrF)=GՉٿ?dOR7݃ J=j^zݚ]-W͝vI浥NpY?y-'*;ST1Ǫ<1|۝ꑮ~:qv<0Fm*lȸ=Kk9]$` X%Q0)~dI#ZCN.䳗։v3R=\cw7Ou}ͿSˉF?0NJhm{m$%j^WoᒜMތ]fj3s/oĘ\Hƍ??^l$i+ // +M32(i22Qzk6jeK^ƚ;1͙]^WJӍoϑ6?R$24?c=dC4TYYgS#7\iLƙ濓on4]ROe$*mx%Iz87FDD?{wlGv߇{0 fp8$(K,K#%*$DJ+KS,Q%)CQ!)r8!0 `}=` uo>}VQZ41N5NspM vv3'c!]w ;bD5D1h&K E+ ?U3`?$t]Ao3W zhGu+Q^jˆtU<+ B[A+^$w[!26  8 BtFLHN(%Ut]_#8+u`c^qbZVxxوj]9qzTo!| )wK T  PZYʋ6 9+u =}Nw+%.Y3G~$qSr}x='-@y{+h#`4 d3`oKnh$z {o}WP z5B$ H0z.Ih荇5>gI #8 zLk\5^x؈i{ò h8V5dDHw%j9i NPuԀ[*p歶xj1wB2qz]y|̀+ruM}t õS9JorbUh0tQ.ZdZCDcZ=WAm p3X\GuC)ze߀vqo&<14#s܉kcS(68M_j&\ۍ jk.[,3\G1hvb{6sXx}^3C!}OwZ.i^5z k >Yp܇(ZOC\b _jm瑻Cv_Cxm4WrP&)Q]&w:fϖp%xF:E>/m;k8@ТyhJ^a0 Wm4k/p= kxl/h7\} 2+X9;!#5Ehv53Àaj9EwixIP0h#:[KWi(cvWt}@4zFQ(xJ Ab$w ](iZ. P⴫pIEx >>S޻ N#w1c8CFn2Ηa78}4PZ21M!ךvumeE *3pjᜮI鈤XF7{P5|ȱk. AQv(pk L!B 2ryؕ}O(]]goe-MB3 .|%of9H.}oXTܙzh31\pJkXVr"]2Q+mkCvm] S 4>$*+)*P\휶P=Ќ=hU,{ngK]~E1PtV =c{gjVrzq5Yx>v)ٸrS 3D 6 B כ)ķ=z;;T}t|Qln ؁m1`S`1B@I(-6`U}ow}ǵS0pmja3`Yyq2PXcr oq=bet w:14j])EFh71m8UHSM5 b~Fgb F2 q_mf9Hϻx'm@Innh"1 p}+ў8Nׁw%of9M_qk ݋(-[ ]HIajkMXyަ [l}eDmzh2z 4JMv|ù-\+zӜ֖o+N4QxLǥs{ |7@eΉQ,@i(Z) mg6vWBi*X}bAN͈ U P TW9N?StM=f:]4n}5D wl*G  >0zk t,0t$OX bp]9Γ:#{v "ݠdE,+6*vT+Gkk{ˈ$i,6 ] fE ?_Π): x3C ;?.?- >/ y7IwĦi m'^o̢dQ:4]`,NXb B@`wvZc #_wVj~x=` f €{Uy!˧΍^6J4lj_N7z8>[7JYx>nlagԓFhOb1^u%2EYNhģF~(>H ɰm;̢j1`78\FhHa$9TobB\ʕcp(O·onWۊSx^d5"i"1 MCp}Kf:>ix # m#8|^`D9`s xF1Ătq>|f1Vb2h <O C輝Ֆ(a_}鑞|na>ǀ{cHr6`~o~m;@&OoC_̰ pYu[q?u' n}I =?\Z ;[ ͤPl!t6QG~ Vc1\4i ;g{`8Ԓq_b >$6=ޏւ1!%]2b'@i(Y) md^10sьh:BN*d' G0=wOaްpx\w)v vy+{chcY|e1z u) ޓɽcn$p+O`ﳇnjPZCnÁ4|dn3Y/i6ֲC L{nCzqg4srnsdBMH5b;BB>v#> Ԧc N@ 4VosIW_<ƽw7Ğ h8{Af(X5*!n raz,!tm}`fPnr6Ь߶?n4#ax358#GDjO}qC܋wӿ|086k7Vr43Yx>n*/,BHD1t$'~yF)hjSLΆa;=ϟaQ#܅Py<`7Z ػb@)< }p4I<'+ǡŀxqy8B*αS3#gO \B+8M'&ړ;ÿy* t;<ϟC@p b}]U]qxt d$0r2C?ÙC(ziؕ&oQ##ۺ뭗 j>;lsN+ ;n:hx>x*1ք&D]Cab'O#3PRG1Q,N1NJ!Şi$ \ӂi>|#|(cK.XOE48c̶a,"{g5ADOadvlH q'ZCr%:Rw삙AǀC1 H`$">cC! ߛDc75z1 EJǑ~uY.)}J1`oښ͇;L2FNfyWBp5,X`#8Os€dPh )%|{4,7\^䰙4N|PmXe+<h z#'zgj n6K g9> 7|c)4g  u##3'{Kq54\/>?tsknn Q>x~fg#*`%rjgLLUcu$tD:=C9B.7oM rAB!p쒕,"(5XN F@z0D(gpk ʼnSא=؏O?-Qp'~B}hQj VBeSut OBp=Q.-w{}ÉS9<iIKy+͞JIF˕;V'8\B s֚n b~b|aBfzjw9gr'2 }xS={M `|(]OCD9nfۇi`1[ Tl+`=}`3oa= C` 'z$Pu2 W4mn5:ӈrRD߱!.8V ;DY%fYeD4 NA >H AG} pBr(?j|89n zid \qg:xb Ez؀9B.p*Ɓ9#xgQBDl;;iG38+Ob} ѽ},D#zG8=X0K&4DW"I +FTCI@AhPR4#sBz( ߖMp.MhV14%4M@x % `4 R`-x B|[Bj4^k MPR]FhvG*3:<[µD:.o=SR:  #GhzPQp8qta\.ΩsnLlx_;(&`'qWГ(`3aݫlMKHE4`DuD:WBh@$i p@AhBwM~w%tC 0`7<(Ēp4]It54]@)@I h[!Dk##!Z4M |Z~=F5xV)[_IZ+{vкpW gOއRJ@Db zTC=oQp⿙td:1Ked {io) (0Ѯر}I'1Ah\! o,~c!:ܩG4y( @ b=TꦦQoog7lK7cl {6 o( d"r @׾濷}kl}l>](u߻wǯ#|FOkۓݕw e G4WwEkm@ ߋbފ}om7D ~(<n7}u.>>kxq{{ {eѻ7 ,Ŏpmf Fb(;9C?Ǟb&ڑf#,ѭBiT³xSh.w^y<ޙbRã\Qv+6)Q ={:fsNb*K%D[ķx5 OYSX;S]@ _ga$;'kõ@NMA@rO%zd99ւ 6 AtI<v"m sBA`!5D0c zQksѭŒ 2Zk&z@J)xQ5rsp"ѝ@ ﰱ]fX=3ul+Zm/pM&;ָHlC~ !GFY -n3݁nMtV~ EVPP/A DZDkAě96y۷x8z{+ í鿛]g5͔T>/ס:kR -Lʯm *O$g!2@t ,9pNU_Oxyc&'B g-BS팙{MtCQQ IDATY51}|-¸Ѧ}q!3=k@0bkvل0p%3AD2POxy ׵*ːlȈR1aXPa!v_]FnΩ@Q3Q֍=L=( ǝb|2JM*͑ VCq90oą_Sso&B "1Da .RŠPkarSo:#'z4ݱ0th &羞|w(DZ5ޕ F_cfa?x+`CFt-~c+4J A9 e3pQ7)p/C*{MtWQc…2 ͊YL\+3`w%ԇz̀D\ WPk~^t0~KR)Q\\RɛMz_/BmS_Y0,`];Cn s;cdQ/.CVR![*lӖ^<>v O\Qma\kl כ4*9&bh̼R埮pF*/,:]U1\ߤx:ٟTf16QB@T|D;OQks\3d j]o#"~rf ݔ!فFh+kh9=u9ēh\aT+?&md ­,v*Ovy,\pIzhW/p޵s2\.D K?ҩq&{ @@AhsM~LcuBPg*>ϗ0nūݛ ?ܹH!Ah _sz>`W*9S› ϒ%TƗ΁M!PGDc|kYYzJsg˘x.4\2cDal %$3.;7]*b˰VJ\dBK@@sM.c-3\߃reX262XKhyv[io/ay USҏmggР%lũ ${W# {C".)&W<>H!i05Q۸2>w pMẃ&-^G)PmBgANho,OV81X3\w6\g16Niq^\2`5uULh.nl@]'KXTA8%kƟ?ZcPk*>ց}1nǀM *&} Qw¿<|;\\߹3SЕ^l|MD576 ڳ%/3X ULZm <ɽ$8䉨zy p>x XT̻e\na欉^ȗ0F>vWvmT׿γ3Q'{7Fa/YKff*?b* p,h c WТh2Nh3}Ŧ;e5,_aR p{y4U"92p/BIB&^Qħ~ > #3 Pۘ|5,nB< v%1%GWNO#P}N᝿CG5Q{#e̽`fn? ?Dž?9j!:g$v1{p"6m QQo+k;[Yan_~?{S;TXp ɷ,AIae3E7 ~\by1ytz:]C QOc# .`m<,]`tsF^zyES߸K_}ssP %#Sksҹs吐̲Ke\&_ ݤm()>@}g!{h4oD]?ZDqc_ "Q{ê{lMLU&C5uyߚ@e|F>~Lk umb#`sg$v]t/}fR&R`]Y11vEKp`p݅ML^Bul{>q#H@iColk0((p"/C ftdfŅkhL{5T\DtdpK':0pyv(0L' ICy)TBjbus* > p!P*+h.G7|c{OCDtvPc] rXm{g1p(´9î8&{LW`dpCx =ٴ#隗rD3&ϘXZ/Cicu==kN0̊ٷꐮjk``c:V09p/ 4heӶDD}D}1 =ƞiOHd",.)-4Q\lQppMxu0\C{AsL zbD[=DtD`{O&`iA?hhVʊ?.@@m@3\=14SICP8D[DȐMD7]z@uCchVXc K!;z; ѸΠE/|TW8ᚶ`~ic@,`'oуA95^(XnFeԩ |,{{"To FL\ᾛbP]Z>,w(zX|0\vs> } L!4ݗA/ }tol!DtW!;pHєcez(=!Bc-/;(/(h,PkpM;ʟQdCDo앤CTo}S0t>k`R1膀fh~\?P[k/W,Xm`=&8v`oL_oa?~,H%EZawk7~y% 8! '^?@yžG8>D:Fˆ#РTk0jߓp>*+ L,^÷$܆Sn`pMmje,|࡟?H?CB"qF<N| 9>dh*"$(wid=|k [uqЋH\þ{ C<A$Aӵ\R)/ r6. m a0\S*_E3 ?4Tz0E  h|]*[__RVc#֯8- @p, a+6T >C$#{ dH!4}\H3SW}H w]M]FсU  l8o PlO=7$%K(NDD;~ijۭ#s0Uq,|G"`tLLBh@Ah9P׆p\{J~=d+8 fŅYq(8  B=4gZY g!k5}6[?αD6zpδ:Xl`&. ](IJ"iy>Fz0+D:S>b)JfɃӠG5M 7dP_qam.!4_PpM=ēܵ %Xϳo% < @IњQfl?`, ~}s`vPpMOVXT";or8xowM%+\DDD @+dm"""k x_#m#g$""b#k;4{5$/Z)4 ,}Bl9 PYDDD JA |h\Ip&`Du"4K@DpM$4$А- 4P%""k N+$ ,ɖ >+l-Y""kN)"fO4̖-Pmonl""k =R:-9@$#e-B3.N]Wp({{!I4l C<7ɶb55@M! P㖆 `wh2@D"WrjMԧ6X"{=?}BAA_Q l@! ` HL "bГ m"ਁn0t =z0mp %A<Gj8?b1\{4I$A'oD >p -kp.s1\DwmJ_-VѢ:4۠@a™v i;VSDFOzWGDΥطT0b<=1\PWB9{5 b=7$~ bߖK(-…Z67E5?ԏ]{6ma5fa) Cs.TCCNԸLvaUDD D[TEPDžl)4 }|ǿ3~hv)31\ bC1m#_=c?Fou;+1\>\,ݵVz&m^Gǐ>25XPA皈h[[ᚶ=!ľ{$c,6 lNDD` ; H׃&l5faD0▼/žOAt[r'.L6첅d55ц@AM(0"L"q1g43KV3u0_~;=0Tlf"`55f \} @B@xO[ (]܏h:0p,g 6,Gd =Z1\݁[ 0<|qD#Lt+o.A: DD DwU@ѤDa,O1\}XMó=!7 P#:թ5ث&ADpMtos F8t 5ADpMtZs>߲Y "D2VU8MГBev ^5}Z}3fs51\.|"jb1\(4gW7,pP\ QBDpMf8ZJ@`(\c1\sɃLd5 ³1WY ""kꐶ Mq1)vc -W`hŠPj_d!N sy ](zSpMDpM^@~,)b˯^a1^mFu ,uc/$r_d1O&;BPWSRY ""kc-956RܺӼ$"b&fj9>\NGDpMroQAG ^e1vUQWQtMPDbB8DD D;6_UNG]E(M; /;KUs-XfOb{vFo?} qX]I,rt^k""k]~):7WnhWɊsqAf1/}̿SEhwEMŠ#*'Q>Z1\D)(g1c LtI!"b&j*JycXyo:kADpMNZa4Ń:V.xQHDpMԆW/7-..Cm|%IzO|6Ϛ1\'kę?zv٩M 3]\S(z1\ڥ ƾ̵*AmE)YȿA:ګ+.@rS|X$ADpMY4Z\ڃS5 "b&0YM!ܺ7&v "b&L/cV6n뚍S(ADpMnOX ڕ`zv~ "b&ϏcBŠ5ma/αDD DecXzs NbжR|As]քᚨ }k oí[,m[VEL}<ܚdQbQÂЖk$ʣKE&"b&r^}plڪ` ^ɢ1\S1 9 mO[8 2~|(\3?@q.NHzuSwoX"bD[9kaQhޏt*g1s""NÞk-֘1q(to cK DD D\18{*l;ko]:Zg1:maУ<_FќY ""k"?)qDQ>d+eXADpMDe@O Mdl=1IgvP:Ӌ^t/NfM'd $[%O/X uqȏ~wuX7Ms 5-o&%Odiuq\?Zq >;!(#Ju)NqqdUY5qf~d"VGe:JؙU-helRS9ςn^ /:G5p|X^*:m jIrePg4~ ׈jhu55}oUHr}Jvw)#hiT+[jʌ 7uJ%@\;IuBzҠ2zӡlLd?(R SK>7JM{53EHH 3ʏՕ˪P2kS5!@PsbUVֵpuVs*͗T]RnnRqdIY2iet(FjXYkTZ,*XZy͞tEnVhnʷWԶ?NiV\yJŒTT4::o:lV'OT__qkV\բ._ꪒɤ08뺏|d2(KD6Z# z~Ir V[[q ֈ:oiϞ}מ竧 1LЂqy;gMFGs=lH\'SSzs'l`\o`9=؍Nك `f z6*;V vww< 6G`r93q'w{`'د2@ UT1j IrZ(~}W~u]fns`ß丱{``kcvĉjooW\&q]yN%Yyn/c#]+JippPJU-h<*ϫ\^ן>JRt-cڔݛUo_p5⺮>VGW__|T*vu5:<ϗ븍/c>w Z+Cԟŏ522ެ}t1r9nqJEsssw Z#7OՉuȑ-?DP(O?Շ:w Z#7OO8\.PXj5yMNNO.ۿN5pc]8Accq5IN P>Ą._W!q4&|^.|קc;q&?:=~#qT(ti3]> @\o]3~[oixxHA(g}ݺu[k1ʹǺSCf۶ܽ4 q+o󳜖`u3 bָ} s@\?!c 11q0qX~B T&ӡA'Qk=݉uIq- Z(%7ѣ /L&u '?}*qq8cVWWE#>0)0Э[BpB @\?Ĕ?;}fH$E#?0p|/|祱$mХP(hF <{"o_hlf~WP x/u]5ݝ֒*f ko5@\5  q kk@\5@\5 ]HQcp]5q q kk5@\5 d$Yx|zIENDB`pyTooling-8.11.0/doc/_static/logo.png000066400000000000000000001724231513317154500174440ustar00rootroot00000000000000PNG  IHDR8rzTXtRaw profile type exifxڭird7vc^ ,cw;,T*;L xr+].j{qKg_m>"sg/?o=>zA?4I~xg>~?-qg#p0EO ɿg$];RR{ww,?W<Dz/q{QB^Nǭ߽{>:UN™mow/Yt~Gߐ#p )x":֒pNl,2?޸]1Xc2^f~=~A p{GP/Wp1-/}>_Jl"兹^/K|}!bdB"TB bc#?ǔ$79JrZc]K^HDI5iK[+RJ-VZeTs-V©aɲf֬hVZmZm0V\z뽏#5~p`ƙfei>Ǣ|V^eeƎ;m m8PJ'rN?Rk7|˭n?_ku7k%JϬq#(gd,@Mr[9*sʙ1JdEA#Xn#w?3ysD[]R9)soǃO7 )hyd]i(uƴRO`]'@}`ׅ2 |ϸm{cI&QL`էә\+3羋Oٗ GȆs:G6"%4]zqc(QsR}4g+]qⱹj~mgk#ʓsynIf;u=z"kgLvǮ"ȃj6g9ʫ߶!_ʇy瀉vGD4p+{;ݱ.eX=εȈO%ßzs/)M{>*""6Lqnw-eb p'ÌB{7b!h\#i>(} ZtSL8}~+lN9̖I?00#ؗ7ӽ% f9Jl0j uW bya;g42o@ 'koޒ- H52@7z}; On4h;EX VE1NTT~B~Nffp:$۠7F6blUvbJLJGǑU ܓi* xi]HsV5"_UEO}.vhS-@ Fizjk mv."˅/._[H8ȝ+ G٪P6ߺѺ~[UtwAF0G#܌,YvXAݣ8wC_{猷Q`%'啝Tt g28mR@Ԛ Kdt 78iY ,dVq(KK!n6(6+% 3*{,UFA*}HzƗ،pǀJ-MN@w(bRdm#v4 V`:5G0 7k[MR%SC Ip|pGTkӈ"lFvI UKi8Є4k4brh[AG*ݧPlECP=EL ]hH:Z*֗1!tZWRV]J.*wx QE$T*bjHQY 954y*pk2B 2#XMJ@fkAQ+$c4Q(̓$ 5SpPb~`:XDP§>iyʣL eC|7 ꢕrYnKS 8=!B H΀׿@̙mdΊlh6U5i7Vjt=RTI\Dhv̯ 1XAV T@#&L#j`g:.TǒGPh1ڍT@w?ГM&-Z׻&@A!6a% @[rӞ4_ CAMTE)m|(1?fϊ~A͡%A5\=jq~ff) DIprAUҥy8iSn@H27^<7B!e@?~j HvhXqD7}NKuq7FҤSϵ$OL؈{jN97$T~섅PΜ(g>eaFB _?j!$D ]aޣ;HdgmlNa͗\'T-(jvw+ɞJoYeA!<<ʡ TĨe#  &TXHE|k>zXɰRj:YxlcI '[~,!tV̏f媄 㘊x Vŝx*[-vScv>ThRCUh$qeκ{MUve=ŽjU[-AS4fltH~v$o: KԴ#%NW 3X !.c V%Z:#:*rLL"cu |9)_:cQ"Op,$OC+rSL{7 j_ o5bN zR FaPy#%uFK{K d9j#Ϥ7L!u#r;~(0|׈n}ڟXU0A`X}@Y$6$T,'!).P? wg^k5 h+]<@: HZP;k"!@@7]| 8z3ױJ!݆ H ,3Gӫ ]p&/%K6!Vaj2R3 7Ł{aaG7 &C@SNm#MDM҂]qzዦv06]GFF=4foAWOAiZ᏷gEi,xaK`:$eڣU/h(ѵ*h2>;50!gH ]@*W,!QMܙц(I{zVG OBu86`{}6C>bMY[oLsW7{g0?!B,~:hcjUߜ]3t%s QCU%Pi^Sj_.!P jbY.0QE/eQkn'L$aD 8FK^%.(Tz>A;H;^I۫ 7l4MoäTU jD8ngL{ }~4jizNоvƟNd?t?AkF( ARfa%IvzZc:*Ik/&%A}y?//1=og|/>Ƀ /1P|O{cOyާx[,ܟ͍4~3_c£cg:3m?}RI' ˿ Mp_.6OmiCCPICC profilex}=H@_S"8dTJ[UKIC(X:8* NN.RBX L&Md,*eR!"1YF'7ď\W<~\pYNR6fES#&NBcg\e{sJ4GH@*J(FV Iڏvrȱ 4Ȯ~wk&`~qQ 4j}8 \-~^ki#nip =)gMY`]zkHSW7!.Pzwf?ƒr0bKGDN pHYs  tIME*;~@ IDATxkgg}'s7fF]IȀ1dm쥲u9T*[rE6UMe7^;y_o qַXs Hэ4h=眼"Tuӿ9<}ηOI$I4*?Y @/ . @/ . @/ .<]wݕ_]wݕ{lYh~:ԧ#iMGɧ?|#I]zd<ַmo{[֭[iuK~Muy'r=|ϻP7??}'t277ܱmsر<#y'{At90UUo~s>d޽Y\\)uӶDn:t(ԧ/~1~{ߟ mz*w_>Oرcy;n-bG]ְ<3Y\\HzgǏgӦMti6RrM7~Ǟe_ρ?9|p>swܑ|Wgg;{>3 k׮۷/& {'rgy;ߙo9ǎ˱cǾ+RUUη^o(!gUU8p I26:3 9|pJ)9p@$O?t>|8g26:;wf/X]ְRJ&''s#H]=\d8OrnRUUsquvȝwޙ;v$Iz|Sr 7dΝ/ addd$w_۷/]}Oby~gRJ&'&r3ys$ɑ#Gr3ɮ]trt mfiieb% <.ȝwޙ|0_Wo;ɹ0έ#wξ}\]?CvL\kuie~۶ivE.;vӧo;[$?[o5;vrU@`?caa!O;Ln[$GѿLn߿?.S.d{+dtt4G<$9zha(l'{7S2;;ݻwgǎIS9tPuvt*_LOOg8{ɵ^{Q.vr{},gr 7d׮]ٷosoFFFrСCwglt4wqG8 )u]g0o}karu ;YXhvܙW߿O9Çdvv}%@`φC7pvؑgf8ffffَsii)ǎ}ݗ9}LnܹE$*9x`>|8LNL;̶m۲m6zB<9|pbڶ}ϫ*0ѣG~ܹ38Ϝ9??pN:ڵ{:rR291cǎɓ']G]?um}.UU婧?vmY\\hyԩ}9zhg޽ٷoK\9p@<׾|07t =!ƕRr={y_{md(f9mz*s-d۶m!gUUl۶-'Õ>̯گ%I4Jg2>t]AzA^t]AzA^t]u@X`ptϻs,/;~< OK+ 7{MɎ+t6ftj4Itpat粴T;Ge X ]VzʞNguٴ.s['29׍\(6>Uelj"]W2a$'͙KL`)uk5{'l;RJbkSt)lg29W9'=u&YcBI5ͫ-ݻ"POT1]o߻#v*&t]m39?|) J.asڛd8lR q%]j)m[ڥ&+ .7L okoܔ4I"wtGJy,m_}TAtv2߻5{./T%o,n8 0ѩ:u]~l?rÇ^f˗~qIU2wn~|g6)B.B7i._'5 *Lۚ׿gG'"sޟzXgtxf;wnK=h\Tu~zOu>돥vivu]գy]r SFrOn,…iXMwJ)ɟϏ(\1eX*%xyMDȅ j0,yț6 2'6fl,F'[sE1E Wy퍛2n(²Um?Y1]~?!esmM)7wmuvt>i:W{K6rᢚ<+oۚk?I18k7d7a߿-7+[וlɛ?3;2&-k /%27gS:)487ljBMX3]5mg\! 8̶25AXe6oRJt׿kC6]6 .5O]{b kk?Ӄ)ͅZ?͗e07T V5A`͹[#?sYK `k6d5ӊf`͙4,$) Beiszl EW+GU 꺾KV 5ejx涌eP-Eȅd8Lv_5>Z.r叮gRJ:Su^>* f{n}ƧjTJ{&slBVA`ƹl5N1XyL6\9nI=X]]5aͳټo٥lmK=zp\8]JUi`tyzPR췲6$]Х*AZrEᗌ$m]m^[J7I.?/Nm>YURL9ใrKKK7WU$?ڶUφZzuݝ;۶ڶt۶Y.^+c)‰6/SJR&YA:Su,RzҴ,5uZ1~mömA)e*y91u;m4_$_W}l7ܶ^=q3EϹmۮq >Omm?d=&{omϖ)zUU69ใf sm~[+]\JmirlieZ}4>?̆Q!eWi*mr.v}i撮;Wäm۶jt>${mޛ䗒l\I~mG瓴=ikii>w]׶.x[۶m9\ʛub;x5m~%پݍI~m{{ݹU қC钪JUSYG k)+]RU5KR5CkOJVKmU4u~,Q$z:hg W]~rуﶸxUUAu$~a?ضW79ใ??Ҷ'Tu5$4ͧWtziiBˠ$]]9Qu[GnhOO%m[҃.IiZ[ bKKo˥2I>ܶ7]׍?:$Y=zeٱ{M۶TU'/$8)9nxÊ\lnh.kV)冶mriiu;gO49b:m]./]rcܬ.]tuTIYKוt))VLMB)IU#I2L m~뺷csE/\𷭻m?߶W{NCm~}ii0qes֧OmTLR>4/E @ՙ0zԹCn%?MjؗJuA8xY/S?Km~iv\UU;r=5cKF?ow;ηLHm)zc΃?9wݷ4EIlSEXL%L\35xR3cj̸e'NűQm-,@"%.  ޗ}m,@b>Jo}9DDP;l5H9E?fE?ڇ"" N3 jdژ vqq'">w7|P+1A" RbQݸ8@lY]/ [Da@o 8X#"7zpdOT<b!$Etn[D=nOD~|+ Ƙ?TՇ8ƉcSޥ 'c^Q\n5L z]ۦ* X K䃪1Vj,)}?Uս1NDt{|.RaA`;v?.DG})A&kA*OY]W L,цZVb]jnLǵ Je.Phm6Ɯ`QGnҫFW28q֯y0sU]HDAZP\kyMspX7(-ߔWuanXϰo}_'q"Bmo.puu9PϠ )_6(senP5Pt~ʢc;#>했g UXpc~[U7}hU 12֋s.m+" Ϩꏱ'I1q'"5>wXiy MD~yxm(et@!)Z-'W2yNo .BDD7jۏ+htÄ~xX<=$bIN}+>J Taq'"z->wT'bȍQ(#aDbb ݻ=H Prhά~z Yt,ϴ02c1UaKh¥!7pޤ(1UOgD䛛 {޽s 1rӞzPb%5?9Ɖc:>wM{T}ڮǟ mF :aB *P*4ZEawV؀@r5rε*+PXՋh.4btM,-J*(Jt#.0Qwr/K_ֶ!Bނk&P ġD2V/qeI WOи S3P( ֝' M5V<vz3a@qA[2``c rxq izEB%Rk \]g J \h8Dܨ/9D^M0jEƌDDDa+]R}O~O8.Gz|܈Pxwg=epOXXU'9Ɖc>wy; E$7W9oƷ=L"A<)X}yTw0kLl`rJZܴэ5\ W WZpmLrqs߽eC2߂c%3Zuƒ&bLJXGG1Rx UPZh|k!ƜCT2UBy0` ec` Vw1 @b:$"*Շg% Uu1NDDRaS& .=DV#5ȏP;0x?bCiPƘ!}F՟%k/$E^i{VGU,t˧,Ηr-vוc(D$=[5W=6˳Ƨn|". l/6U@h1ryP/E#0V bZ₽6~ccT!`tw:\CKXԂK6Ö([Fs't^:jvKpA(NpST?ڶby1?B1NDq~8> "Bz= dhQ*#?XDu{ oۅX-z9wiF'1|h9\8 .~WZX6WyIvá^6MDa)j FJ1V;Pcd{ E\>[<Rl#Hzu̿Zǹg1|&/h3BDc8qºvѣyͦ{0fK93?Y'q"">A]fccѷu+JwZGwb;Z ~$.8 hϴxIJ͢B7M/;42g/ay2UxbP*pK|qb`P{p}X痰tKgQ6a'EdZUZsb`}m(sJ]9?s8mP#`a%ԫv~tcoGpXG~>|y|BHO{KT1 dmӢnQѠPqC^EmB"W`Zo_,W00YBc-Gpsu\9l~ODDڭ{EM g8C6BUme~X{7c8Ɖhls{<®7 K`rE7cp(b #)Dc7-_WEg"inWnF}QsAdzi]֧>|Ey⡏mCybtgQVѝ2V`E\( 08YBÅ8XNcs/X,"x `o0DDO{p(z,""NrEe c8Ɖh#mshkt; 2]á(CUEaD:ُ{;} NXM勺Ś|U`Swf`=DbÇ <:R-tVoaeid`#(gA먎pK'l"" Ûv-z@PA9? .b | IDATeЅ}/:Ὗ`%lWa1NDDAf{)ktIwmžET#.À˦-Jl}xO]w~|/7 ߚwXXh7[d׬k7C`Z_QDD9l=҇ w%* ,E(ø21 $"J7z@xh+]z;'"8)˜sqeWI< 9Ɖc6ȃl/e .)5呯EQ ` r}_3]gk[! V]Ѭ{tBw+:}޷+m{L=XLFpjgVYuPW.Dz[골Gv:sy:BقA*8xd Nx؈6u䃓Q:o.>Dt^U<kb¢\m@MDFUu;] ȒM{c9}p#=4B xFղ~ +"1y$ɣƘ^AU-Hccc!-/iU`@@[/lO0""ke:4ĠF_:E#naCed`$w4Rc0E]CѪ;MY󀱜+Pxo|%¶l;2rE"*wHu;D~)YU,0 h1pt>2t(WP=)+0QN}P5HӶ + 5Z Zf'Ȳal=2\j!Scj2gxfֆn~5kD4//꣪_U D7 P@n/f|Eg퐎'xԤ5o1^-Zk"M~{8999Ɖ܁kP/ȏXkWSضe"Џ\aa{ C.G=-(p&ރƊ*ݹ\]#% ebـ!.2Ȣэ7_־'nh窈|l.ncEι-h. WEbƮS8899(c;𸜅nj|"[yZnb^t ~MX~ 1?| ?0 ǷwʝT284EA $Vl{|Ǥ^c WP㱟ځ(x(ߊ~%ZokmpA95Ђ.mI=07|nl˘_繟ccc<.,+U83A_#٭ j GTYoƁx3Q⹋<hG1{Ja~ +1llPOlÛ?X^>W~]',f?yJPS"_EDHGO!>|FDNg?\"888:>wB<"2?A.j9l>L<\7:ZSđ0\EXx΅!I$.8 <1**b$ȗ#L;cE!MߊsD. 6A8U=uf>袪mW+^'x ![c8Q ڊY\4=-V %PTPu8sk7l0V-ve, Qϝ/ OAݎVZ-mʠ |m~*}\'^A\h)oq&SEg3~;^'" ([`ǃ̨j_Zu0]61 p?p;?5?fAn>iĂ]:u)jO^9 u o Va-{ð Q\  {.#Á5,q&l? cc(>w,9tO[>;{}S50B+?PַO=xAsţ8ÈKՖ6P贮WZh~Wˆ1]z"ejȍ9tGSދ-?*C.3=DJcnŮFX"'45kspk*YG!QXq벁jTC QvEQ|pׂU;w+;\х(D$㹟cc( |@|x ܃m8܇h LPwZ%.1zx v~088\ԝUEcwBW]S5C.- e֕gDDQ۔9 g6Q1BDDDDDw(3$- .h]ҖߏmGybX>W.Q1IA$MΈä(?lQ^jp% e KЛDd H9ڶEe """""C|@AnĤ]](\Ʌ6"C'㇧Q߳pml]Mk;} a-틲tÁ >w Jffkvakq5G?]}m,E/Be81VK+ rXRv!uºMңӁ-"""""s" |( !\Ʌ6Z%*>8=8hbcEW_5E Asa"c]o*.DDDDDDDDAxd?0eFJ U@QHʬtƉA @q)7mOO/V *1RsΧMNTm'Et; |!Rof}/:tfE"""""""( ʡv}o3.jka؋3:*T6ǴOC7(D=L.* !e7~a"jh@٧x"""""""H |.  qm0VTw d'Xmrb0V!):CTeȮGc ADrz,[ WtqMVsn[DDDDDDDD.b;:X~([\=>k|#Tbvrx5fH2s &t?Y "tjΠ m~kN1򮭘z.ʤT ?z}P%1L$1];>BRJѪ'.DQ0x FDV"rGUK]j9Dp P.``Qo.O^]hd>"?]Zn@a;Zk(^;rE.쟞 +Xl92-Y"\#}^U-V⠂.Qq""""""""4D<O"*\n?7f۪*P]R[@Rr{(˖.71{+6Y "%sޣ$v2BDDDDDDD.sI*F܂d?{ $6^oL/3B=3yyb ~""JCcBZeS^ϐ.^, """"""eÔ<s)x7:<xp IDAT+_Y⠸Lbys l @1T5" U&kh)\hsض6f6Psb' " T)C 0_Yw t.a*T;!U@L3b~Ki*t&6NXc!؜& UF6ziX3h'x9E:̵= |p_ v'"Fn8$-(}wgWcb,v1.@Da5) FKVZHVy!""~(/HsGUu= 3&! W. fVjpaй} nT2c=n#틜si۾(]t!""""""T :!2sj ,K6k ^HܲPQ ' f,2p n|.a&|""76X0y#W &?NDDDDDDDit%*\ek'XՓ\e=S\YR0Auj}kY ˸°K&~f + .</5p:!""'4]TUD`(EWt!""""""T ;蒋  ibIW=_AZ@~ϣH.pvCt^IpZ+ܳTmU4m]@9n~EDf9ډ(M Lnke]{^'1 W ,l.Y ˝m.<ϯ;ݻ'gKX#""j9HSTu$ qMԿܶR'ؠ) =߁\Y?qKN^k%=_(4VAn";6F32fVjH\İKs/`}\YŋM\|bU %$IRmNA\_&=߁&3+]o=3_~HAuc\k+̬V0:K^,. xɅXo1DDDt7Bi1&- .DDDDDDDF]ߵh|E\.;¥g^E,D2c};"Q++̯3Vg<՗]>Sߜw>?bݽ,#)iG0Ak-.DDDDDDD:A]F߂C']|Zjpor܃-L.+)] \^b~m33m`y1Bw%I/8߉<Hs7=袪@uYDrQt:4\1Ny rso-LdQCqW5ZإU]rK EwƥSKʐ=Q4F܆]!KD RAx?Ll-wp_t5H}Խu [g?;$y:V~DC/lm Pk. Ǯipߴ$Oӊ&u1Mg6CY ݗH}XZ@ )u싻uS d@}h/awb!v/ujBQ#BX'_$0jt%HEûK4d,L֦')+StF%.)5??j)f4z%HY' rv?xmlX8޾/+)t3H]a\٧:TC3dv~%9sabJy.)Su1xqB]ٺ҅#%զb޳ ddۖe%HC'4TZ?Ac3 4\FkI쪖b]8ZȞ `L@ZK';$5v`Xt̨:1F^.P] 衲?=(Q&+&8[Py: ,0 3tqKta"z hk ]3e,exl#\p".}}5rdF AfĮ gun-Rt!C+{{2 w 2tiljjXZ!˒JLo5*b,2vIcE8$Z4}3F1XʻR-_tLL)$ Az 5u !בoWaR8G%%f?8SyhTq(ƚ8[Rꐂ} LdI/8+)t]zۤ32Y7"4Wb&K}]R6WCe. WysuHQQ`)%IŒ.>e%r&`v 1+ř]Ɗ](vIn͎U%vGCqJ5~BJ%^Pܝ+l. 2tɷY-H ^7~5WK](P]R/~rʅ:h$vEX`9n̦$8/[3rEdB.PH \/wjO%eSKCH"`Qg^e E]&vIwe}'m SATI3a $3WvI_.̣aߜyRJ%`=Q zײ|޻hט[3q-Ό. 324>]kҵWG̰ׯÿnY`cy 1s+@.MvY }|Z}&)J&_WRߣR} #CݶH)is !A $l/Urry~F3c,4z/˓e~-wx.2ZdW8Oe9{.r+3]2U}9 2 0 3(IZܴH:N*طMάf슫\Z y06WbK=G]W§1V.z,c&30E q%׾/7 CJN{`-ֿ.'/y.2RtKZxjZ;ZpWք@+Zu+rr,˽Yguq틴[2XdJ.cu]^Kx=QT7lKT=s2ɘ|kw(*`YSt1nu3L\Х^7E,ަtju6=roloT4V`/ʰKLeUǧҙ=$KPл~2p S݋환% Ýt*Ȓ]%qs,biԿm,dɅT@~h}vum*=^TGQ##{*_;g>~B1we8mR[WNkog9@6 wdaq/E-,I:H,\e섒(nԲY](i}kԳy6:ꅪftY\eأba>zA{U+E$#:zZo (KF%8-|]53.j@dԉ1MtͫG_|=m߰VW46J]^f6%9Hυ]J]( "v?gj#,0uؤ;MAXz_h.qgb"3c"9 L7r}]{pOEv[s梋A$"䲤jsEhԩ qH%%TrX:HzVUXb_t"3ۚ}YD\Y4]֜YtmJۯdIMԒ$˒{r ]RS}k\IH\K<`e`  6mȾ!2'Ӄ7Cڲ^UҹˬZ;ԹzZZPp)Rha%a9Ĭ>oS(FCGouSR;= AXdM&.q=mB z;uj.˩e}zVj6T+TU-'&+]RfT]Y+zVSffʒ}Il[&^Ie`7ɠKm$kOҽ_}t2;ջytYS4r. vIk$#ui.El>a"ILKDq-:t^nw9^0t+Ҝ3xL4fXf.N9.)z oC|Sx;a}GƸ 3qL`e22}j\Q5je\jiӆޥ߷^bZmrMt y8$+@NMDSki]^ݢ̐Rç^@ IDAT03Cn ufa$IYɧ;S.(D͹r˵lXؚP4%FH!\+ukاz'̊S5M`ʵZ{o K堤ٔ[ҝFL^-@e}lYI-R3?\ӣ;߽E}']D;Mw(זHׁ\FKݚ*vYNC%-ZK[drFAXdfًig^ݳt9ifSt&ȢNMI ;lYCŴW*l5w!\b/]=.vYNefMZ>NAX ,,_m/i]s *AR~ס;߽U^y]Q-,.Bsa.j43dxk ԻU}d!%`_"mؒ}Y'S'T-VzY`\ӭ?Gcf/mz6]Dn˲Z%0iuۺ(, %IsgFY}r? UrQQkOZ... cug3Ke@xYe˒;$֙`R=ԦVh|^sR>̻ܽs?3aHdVf.Ś0P}kGcV/Em;92FMz9.K$Bd嵒 ۢ5wucEfi_(.K0Yk ۢޜ:ֆE_(Imؾ]̌\@e:2qlD{qN.$|y-m- #2~ 5^\AeL/S @Zժ޹Fk_ס|w@QX$I0o)%N;Ai~3c* M+X"BZ{ڴf&~F0ϋ3oQB_Qzz.2Qxa8;a4s"QLi;ng-Œ^ꓭoE,. 2\&^ҹG6 1ʵ)`oM;5VX8!처(QT',5- mEbf|5wl>p',A#^΂@R[urShc\eܡQ.j|EkD~>JAXdI[oo@4ʆNj,ct%nߨUoMP+ G}2v)vks1tN>5lBd53ozv  03(m7g=A3c"&0>0ê++wr.Խ?]-(^w7$ֳicw.=tPy&:8Ȩ\>Ъ;:݄EX[ R>7gtAM\4;8z^CBB jqOzZEsͅ]K]dj%f!4tԵ6s( zofY]} zSm;:ɕSe/o[r౫^շ p\$P-a0e Q]Z^m|C6.,轙{.fvCA(0K$i/]AC=Ŝ=<#@jZ۴=a{zt{Ҫ׭Wؒ1LAUׅ"V֡5wu)f6.=IA(۫~Fk}s75n[8h^I 27sd!rX4z̐bMѪ֩{%Z{ڵ{a%J~WчzB.Cn޾/@!2*[ߦW=Pe6V5P{_NqK@&STK\K(]A>'l 0Z͎䉫kuJz%VkwNAhcYhjiU//-]A` z9'|G Oz9V5PTKVU$ޜ,J%P5PTw..+%$ LHq^IT+je:]\+5Ekg(La>P¼)lXzq̒4~UUN2toHr;$I8&i5X.8(ICݼR˙$Vth#J‘ o{H|8"u᥊&JZbdP6<اμ+=T\Kd)JbWdfj,0ZC ljQ-Q3T).v\ DXAL/} 4K,09G˹PfRr-jXA`jɫV_G]cKz9V\'Ȃ!z%i UJ"W3.nK\O!]G05$̔oƉDa.PhvXU %5W<)nQ&8>&q/_ɷ~:ĝth r=C}zX\إS~]]@ yJcb`~,se<?;!OX ֎[(4wO{e5mL{}0IF4N֩'O0 0O)*)0OǾ6/p=I(Z@ p B]ԑ \g]Xc;tr-K\R]թ*/pN|q>|w@14r W8fƲEzZ8xrPw3fun)ߖWKH apGA3$LjS\@k1q*N}Y\OGA=c:}%-LZ[3#TURD!庇蔥tHlUw.{g=s:nࣩ|6V$z%RmF-9,1H\B [˃i]$]-5u Ð +EQ.âHg<,c^n-H*I\VUVWˣ`)a8!b颹Ok=1OI EՕApo&L]UR'3;jkM./ߙT.%r%fѪL_ǰRo7켤~wZwKjKyMŷ2ߒAi-d!.pL r9j*%GYm)(hzI*Oub`\gwϪ1/N(]" ҡ'F5RIq3fzw3wEI{msI?Wcf1](66q|{h .w?QO6J쯔|yu늊5j,O0QhK*w&58PPT"D.euW,1lHָ:IHjIk`rA4ta/8 A'I/4^w7?ǾŬ. ( 3)Ox9DXũN>;MH,O]o!}GwRmxK긓n8f_Co8=N*pI(]$ϖu*]aK?_Ƣ>l)מ"*]a)R 4z5xUwNʸ*sSf1)-'aKlkȌ.+} w7Iqz>po$Iߗ8@G s=vF.(p>D 5e)- }Z6 ,ɣ5=ѓ*MTWkfOhhQ1ER4$IiFu+t$i4'~Ygt$Ww4xX҆ L8$I8SV׮8%IfF.9!=ˬ Y8^j,x=,O?q0 &IH$8z]޽L(_$I2$ÒzWr\^V8^oUMw ',$L,OjmP#G5ә~sHfv&pOKh9 ^0HJ`JȲEk_B]5IG8=Nƹَ~)_J$p&$$$% Z%IZef$I65s].WQqXwuݶnd23bK6<Ы5wuS d\HRf4g !f$Iq  i_^g$,iKVktwk#o3PB.1'O{̬$,cq^\nS Nws!QxUw_S= PC.-vQ;¼z ǩI<g=$>q8U`Fki[AAI.BKl'Fn54shlI*:oRg/TdU3KDq5>,iLREB%4]A] 3[w$Ir捜2q_Ě·veٌ{$EQ}*tR{Q9$IS8<}D8ax$S]ua}Z*h(hz&Bvxj&5LՎ|DfoXXךq<$ismV7Rv>h6:G8Wq|P6kM:}:2$# =ȿq^ի,!lr9w`J_ǧ) \C˒$An뾓*KNP̣N&k"r5~ΌJΚ`hN.<']:8EqYĎq8EM ^^ MH]uw̧v&>āD<7n~G& L))&4txZIBnZیWE7gF(40؍ϋ8qI:K%p=]nҹO1))r:*hWI+\|bZ(MܮaI%ʀ[0#=z*%t#Ϗk>czDA\guA 5~qR/~S| ~cߦ W 3̹,6nwmfNQ\A[4s]m<\d}r! MN?UЎOվo ^ef,M!)IR-Jn|LXݗnoD0i*k< ҞQg8TA]w)>fpM9E5.XS!'0D%nL,], }XJ>A%pkJ0>~P%&.&CrsyP s:oJqKt^ ?*\|э4ӔXcqInn3;KAQ\?(8i.Mpq .h@&S.@}CGtbǘ:a,.B. ja27TXX>R܄{ݗ*!貀ƞ9AX LױQE5.X!X($K IDATÐ]nNX&s2 7)M pTJv z].cv4Ȇ~ㄞ k {H\J oȃOQIT5;lǩSf Aprܣ*Y4AE0{pZ>O8 f@`Y. Sv}bTa S=_TP %q2[pIѡoi'( PA+I:LW$޹̞Ap7kTbq$I,;Ǯ[Ħs_ġqknWb!AvoXLhǟٲ~dcaQ5Vqf4Fjl(2,a~2}qr6J̒(~jj0=zy]3w#j,=|[qgn<^3%>) D$.2ө㊪1kWc͌Vt~n <8ߠ0 zif,[@.3v=E`^Tbadu]HRItk4~hX&Ȓ Y6bYq=*NX.nz%hEɁI~Fh˾$(UX7r_7F%).@JaGJu3ez8>G%́0 &'rF{iM:t ~ȴx|삾[T'삗Xu :ⴆ_,SoIK%\) Jn%0 )af/HTu3;B`GTb$,8LeDu:]>>Z(e$]1BI?ԱF)䉫2[ixa\;>5NNB.@2 5^suZ^)?yPĨҗϪ4Uc&Į*Mtnߤ݈=1hD\YD%āuqvJ!W@*ϡ%fys*Afv=] 1͊$ye}C@m* :_ұ/)NxȈqI\4'z'5thJ*W5G)XEM1{|D; \xY.{>K%L?lf֙YY>**,9w h:uw 3{=Wp0%H5n/'z}ѪN}vi;7ޮnpRp/ +ηWijQ_ L t1Mܮj*!W}P6>r9K)ĀSA.@r?_oSrXfvߑ$ɟK_ujikQgNu>M֚7mVUZ+5v!45MǵV=u{VK,+MT*UuQΌ( z`^?ǹ) fvR,-3y*񊻴ݔH0 '㒤ߗRUtrXZfVQ}TI: kMʔKF4Jݛ;uSU\-/FB҅Ə{ zkjs[Vx23M/Y?2eMjv0@nH>fǿ(O {ݟf6B5NIP3YdzXCE`?bf/8=rroQIR:}768ߵFRgdqVTU-F;]Љ :쬒+:5p0pUI+U_i?R,),IEw'Ր$@c I;If٬_O͌,z }%K{z]ag1I3 ͌H gX^Zk_Imk$ x|b$z uȩoCV٥5wuvIqԓ1$y< ǚu6. t3NJT.Im\ծUnr,#yryJ ⪫<6Sԙ?=o҅3YӦuU@fFVʼnkBYr#ό0ZUa)xs w$IIBx9 cA|γi"8:lX6i] lM$cIkβ+/ }zSII3-?{w$y߇{fA( )HY%v,Gv8cؕJʮT\"o\e;qGǖ|(QO @ {cϙ{ cN|>U;3y=;Or[$[LԹFi$YN2l)lhgjOw:J)ϛ9AtfI'ޝf6w}Lښ_)4\.Sӟ}s{.we۞箔&)k0HRr\.Xʅ#K;9Hp)0p'hR4J) At}L},]k$${UgM-RUk}Z8l*5'1A `x?O8w{wgݷLM4M6ʦ[[wxBV$d ~Rosbncs̖ݓtKn#K5&.fR?>8S[LgآX XLZjj?u>ZPkVJ)e֬nZoo#0Nf?|)[.L)彵flxIR>ڶ77J)KJc[Ae$y_h_ڟ[>pgz;MzSlڵ9Mo52 ᗒՎ#.5k֒$Q YZJ9oNGGx/$Oغo0D)m}Ymm}zj{>/\n~*ɩˏ:Ρ$R*k].OOwx_)I&)Iiқ;Kz`Y` &5iҦa}s:%nxf$Ksy[LM5ټ-;&3dӤ֚:L6a˥3KygtJ>69fӏ0ϗRxZ 8xVc5FaCǜh&Ms;nM[SijҬ~\yV0.ڶ&$ɰų39wtN}x揭uM2w͞w{՟Ǜ}eYSҪpMgW+?}!\1YɅΞnJ6o2iXIF>XCuL?bLjTj[s;ud&teD&2-;'3ie]lst%kA+YYdfos, r\69; <\zK۶q  `fم>{c,ݒMtz%CӲjR26'BJ79R06w21dSql3-R]6Ge$Mw ԗ\U֤,)M.w#Kdfrgimf>G2}|9&Z`֓p}We.'X̵9{BnRN)5URF.V`zzᑥ$I3Qv6eR{* l5Ҕ, uD2Mz)]jHeXSa}q++$s,Ldaz%st,/ Sۚٳ+긒?m+R>> t:.&2X.5WF-u"9ON2w|.'O锴+5fr{NfsǶf^ANMA&tRkpdDAVٴs23ew/fJfdTӟm3$gWLF(]^JY1cF'عxh.'Z!0gM|bDta՚f8HStJSҝ(Ć/)%d a 8jkOiS틮q8 n*|p4 @]Eݩٴiݕհ\3$ÁnU`0pjf @Ѕq\M͖bn~67mKXZ4Pm׏ϏPR chX3sz9ťlXDg(Qd' {<"me]%`Vu'+;skJۤH))&-Ie=ǔ)]맻޶mrD+p 㧔 '6gzekJI* qԚ0oPv^R&R$K=BO)cmۿ4Bc}ȌZӶȍERSSoAkZےD[JrYib0ykݒϏxݮ.ZA 0~ښ,-SvNwdoc)Im>-lKb0/$3"=VJ9o5u-XjFz{1uRKJ1ZIٕ Z`Z%k#ke(ZЅil^$͍m3I۝H5# s9LSkIvxg0 qK0~mڥ唡7鯵i*7նeoMgcR@Sҝ\Z믌ҘB4FrܦqSmfRuXmMwLq[sOޑ8=(M"`?oذ%fԤ3MFZ(&7qưi[7p.dR06iSIz#켔/R+Ϗ];SR4{lΖ;ސ}Gwg}7m&L 3\t`\M6Z j'y]Ѕ3\j3wb&fcі&[oߝ?{O-[߾ԮB0kk5y)ʈuriϛI`-mWGt.:Y3ϧIZ(K W4'.b?M^A!٧SZwgTU- ta gShӴI]U;W>taguuWSt6 m[S)w{ A{+mRkj5m_7K)2N+W>taLsG3R$yOH,WJy^^ӯR|"k0SaJ> . Zw{*ڦiKta>u1'}.gN+ex]k)+}AЅ ag8HQHdtzA1`m4'K)ǔulo]0枟K;7&5U9 2'fkb4jXn/̿7a . plK%H?O]Hi揕R~W)u&6/b#)Y_J( U95MR["†xb9g/S O%RNbIIZW:Ru kRܶJ)ZPA6һdnU R|f:džFW<_JyXNRx]J)Z>p ]|lv}<ޛaGA+Rsع<8&YNB)lTV垪t:<^JS6rZRʩ${g{0pAHxq1MӴ;v>MaU:XoQIJ($^O 0VJ 'Dy.lxS2Lsm foKtaûb|{_K]^VF^'s',Ƅ D 3 [`a߾;&tװxt! a*Ia%?oR` tWf`]5Yÿ ƞ \z*3^YxtN3l.p̿0 3 9է21UJ~̝7b}㙼 l.ӫ]YSy6b A?LP\R KK9#y~#oO=΄ןs;K*kh#i@z&skO<}x .pܑ?S ‹~Gb"%ɉ94%M ѵ<9kUtgMRr'S;6eXE`, rx]:*}&m}to-) 9D.>t8G~ﴂA%Io vU7raM+_|O&vnMj3+99>Gv:+ϯ5bή\O=wҟO)˻rB.o `B_Oz.).cqp-5e0bGܯ?(o`e"g3mN)I`B>{0Ua"X]`iz͓#s'ߗΖ)uMp%g{(~^T#뛠 S[ٚ[R;[˺SW[Yz|| j0]`}tulm;%M&f䅜\|\_H\`D UXO,gę\xb:ܑDIW",4uӏȡO=#iW\֒ <ܱ uGt8W2*u"#d8?0'Ϝ<+;ٛܚH[Ku5"՗ԯ۴Y3Oӧ2z>srvE3AA ;<9\v\;ݾ%ܝ lԫSjj-?79,]P$HFɥ:y, /fӭs'Mlْ{xI^^.mVfӿ4t'sGr?r)%u]`L<9'/wӶwlڜ]vVQ*IJ,^XH;{)9Χv{9/آ`t1sL_ldӖliS29fr2^lK)5u0Ldss9Щ ^o])Ac'B…l3ص%oۙ-vt4/no4͉a2?S3YY ;v1g`9A#0FUХm mnoߞ۷뙚>$?gyLuty񃙟ϙ3g{7z]v#HnyoJi] z\4G}23.err20Mdt:W}]ٷo5 я~$IC ut/~|tfgg$vvy#a$vvI"0ºm@^7 G?/|Kٶm@: [wAwv̧?{KͽȾ}oˮ]:zؕ.GlݺoK ZAwvԧ>sKtgn`Tutv9|_ζm. F.٥֚`@_ݻ0F&j]ٷo.c;j~yg[^>={˕.$G4%> :4rAwv+sssI|g]v#pn޻7m[]֡:Ժe=vXg[o5jg$y衇^֑ Rg$?F.ɏvvi"tG˗lݺ5I21˞={6F/8m[uvX'F>.g3??R˾}KrGw$tXrC^_rmwyCvv%O(yV 26Awvԧ>Ԛ8p {C]J)ٵkg>dYXXtv^e˖-IjNvޝ^RU]Tn,/-h./۟\ww_LMM]uRiӦiW޼7^+} ȑ߿/7߼7{555͛7w۾i64ttΛ/TS2ћHogctѝo-]wݕ]7Jƃ*Y\\l]pEW,JVC.Oyߝ={v}c7yyy93339sL.]bn 4OB>sݻ7ۺhii)NʑGr);/?=ٽ{$Lfzi;|ȁTeee%9|pm+Zuryo?U|Zvrγ>@? +`٠Ztr|'ַ:֑ U'o:C#tѝ\zWh,km.7K;cǎmZu4M<Ӈ``ˍRӶuA4z9vx]nt'ڱcGڵcǎV/zrE۶7&׳ z.멓4i6VPme=vrYAg]trN.utײn. ۷owܖcٟ{ケSJy׵\J)iժi7\Q.z//}ɼ]̼~izV^wC67º 4M \t)0~?m۾%KJIڶf0رck劙;v^/ǎ̌ l(*G/}9ڵ3^ki63y'l?$Ɏ;ҶUﭺe̋c(J:d ^7`7Ko,ce;Εi@F!d]K\QF #A Ht`$ܱ @ A_Q4SEV t A@ B.$]H t A@ B.$]HX3ss,/콟.$]H t A@ B.$]H t A@53 g@#IENDB`pyTooling-8.11.0/doc/_templates/000077500000000000000000000000001513317154500164745ustar00rootroot00000000000000pyTooling-8.11.0/doc/_templates/autoapi/000077500000000000000000000000001513317154500201365ustar00rootroot00000000000000pyTooling-8.11.0/doc/_templates/autoapi/module.rst000066400000000000000000000053001513317154500221530ustar00rootroot00000000000000.. # Template modified by Patrick Lehmann * removed automodule on top, because private members are activated for autodoc (no doubled documentation). * Made sections like 'submodules' bold text, but no headlines to reduce number of ToC levels. {{ '=' * node.name|length }} {{ node.name }} {{ '=' * node.name|length }} .. automodule:: {{ node.name }} {##} {%- block modules -%} {%- if subnodes %} **Submodules** .. toctree:: :maxdepth: 1 {% for item in subnodes %} {{ item.name }} {%- endfor %} {##} {%- endif -%} {%- endblock -%} {##} .. currentmodule:: {{ node.name }} {##} {%- if node.variables %} **Variables** {% for item, obj in node.variables.items() -%} - :py:data:`{{ item }}` {#{ obj|summary }#} {% endfor -%} {%- endif -%} {%- if node.functions %} **Functions** {% for item, obj in node.functions.items() -%} - :py:func:`{{ item }}`: {{ obj|summary }} {% endfor -%} {%- endif -%} {%- if node.exceptions %} **Exceptions** {% for item, obj in node.exceptions.items() -%} - :py:exc:`{{ item }}`: {{ obj|summary }} {% endfor -%} {%- endif -%} {%- if node.classes %} **Classes** {% for item, obj in node.classes.items() -%} - :py:class:`{{ item }}`: {{ obj|summary }} {% endfor -%} {%- endif -%} {%- block variables -%} {%- if node.variables %} --------------------- **Variables** {#% for item, obj in node.variables.items() -%} - :py:data:`{{ item }}` {% endfor -%#} {% for item, obj in node.variables.items() %} .. autodata:: {{ item }} :annotation: .. code-block:: text {{ obj|pprint|indent(6) }} {##} {%- endfor -%} {%- endif -%} {%- endblock -%} {%- block functions -%} {%- if node.functions %} --------------------- **Functions** {% for item in node.functions %} .. autofunction:: {{ item }} {##} {%- endfor -%} {%- endif -%} {%- endblock -%} {%- block exceptions -%} {%- if node.exceptions %} --------------------- **Exceptions** {#% for item, obj in node.exceptions.items() -%} - :py:exc:`{{ item }}`: {{ obj|summary }} {% endfor -%#} {% for item in node.exceptions %} .. autoexception:: {{ item }} .. rubric:: Inheritance .. inheritance-diagram:: {{ item }} :parts: 1 {##} {%- endfor -%} {%- endif -%} {%- endblock -%} {%- block classes -%} {%- if node.classes %} --------------------- **Classes** {#% for item, obj in node.classes.items() -%} - :py:class:`{{ item }}`: {{ obj|summary }} {% endfor -%#} {% for item in node.classes %} .. autoclass:: {{ item }} :members: :private-members: :special-members: :inherited-members: :exclude-members: __weakref__, __init_subclass__, __class_getitem__ .. rubric:: Inheritance .. inheritance-diagram:: {{ item }} :parts: 1 {##} {%- endfor -%} {%- endif -%} {%- endblock -%} pyTooling-8.11.0/doc/_templates/autoapi/package.rst000066400000000000000000000003731513317154500222660ustar00rootroot00000000000000.. # Template created by Patrick Lehmann Python Class Reference ###################### Reference of all packages and modules: .. automodule:: {{ node.name }} .. toctree:: :maxdepth: 1 {% for item in subnodes %} {{ item.name }} {%- endfor %} pyTooling-8.11.0/doc/conf.py000066400000000000000000000242051513317154500156410ustar00rootroot00000000000000# If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. from sys import path as sys_path from os.path import abspath from pathlib import Path from pyTooling.Packaging import extractVersionInformation # ============================================================================== # Project configuration # ============================================================================== githubNamespace = "pyTooling" githubProject = pythonProject = "pyTooling" directoryName = pythonProject.replace('.', '/') # ============================================================================== # Project paths # ============================================================================== ROOT = Path(__file__).resolve().parent sys_path.insert(0, abspath(".")) sys_path.insert(0, abspath("..")) sys_path.insert(0, abspath(f"../{directoryName}")) # ============================================================================== # Project information and versioning # ============================================================================== # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. packageInformationFile = Path(f"../{directoryName}/Common/__init__.py") versionInformation = extractVersionInformation(packageInformationFile) project = pythonProject author = versionInformation.Author copyright = versionInformation.Copyright version = ".".join(versionInformation.Version.split(".")[:2]) # e.g. 2.3 The short X.Y version. release = versionInformation.Version # ============================================================================== # Miscellaneous settings # ============================================================================== # The master toctree document. master_doc = "index" # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ "_build", "_theme", "Thumbs.db", ".DS_Store" ] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "manni" # ============================================================================== # Restructured Text settings # ============================================================================== prologPath = Path("prolog.inc") try: with prologPath.open("r", encoding="utf-8") as fileHandle: rst_prolog = fileHandle.read() except Exception as ex: print(f"[ERROR:] While reading '{prologPath}'.") print(ex) rst_prolog = "" # ============================================================================== # Options for HTML output # ============================================================================== html_theme = "sphinx_rtd_theme" html_theme_options = { "logo_only": True, "vcs_pageview_mode": 'blob', "navigation_depth": 5, } html_css_files = [ 'css/override.css', ] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] html_logo = str(Path(html_static_path[0]) / "logo.png") html_favicon = str(Path(html_static_path[0]) / "icon.png") # Output file base name for HTML help builder. htmlhelp_basename = f"{pythonProject}Doc" # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. html_last_updated_fmt = "%d.%m.%Y" # ============================================================================== # Python settings # ============================================================================== modindex_common_prefix = [ f"{pythonProject}." ] # ============================================================================== # Options for LaTeX / PDF output # ============================================================================== from textwrap import dedent latex_elements = { # The paper size ('letterpaper' or 'a4paper'). "papersize": "a4paper", # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. "preamble": dedent(r""" % ================================================================================ % User defined additional preamble code % ================================================================================ % Add more Unicode characters for pdfLaTeX. % - Alternatively, compile with XeLaTeX or LuaLaTeX. % - https://GitHub.com/sphinx-doc/sphinx/issues/3511 % \ifdefined\DeclareUnicodeCharacter \DeclareUnicodeCharacter{2265}{$\geq$} \DeclareUnicodeCharacter{21D2}{$\Rightarrow$} \fi % ================================================================================ """), # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, f"{pythonProject}.tex", f"The {pythonProject} Documentation", "Patrick Lehmann", "manual" ), ] # ============================================================================== # Extensions # ============================================================================== extensions = [ # Standard Sphinx extensions "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.inheritance_diagram", "sphinx.ext.todo", "sphinx.ext.graphviz", "sphinx.ext.mathjax", "sphinx.ext.ifconfig", "sphinx.ext.viewcode", # SphinxContrib extensions "sphinxcontrib.mermaid", # Other extensions "sphinx_design", "sphinx_copybutton", "sphinx_autodoc_typehints", "autoapi.sphinx", "sphinx_reports", # User defined extensions ] # ============================================================================== # Sphinx.Ext.InterSphinx # ============================================================================== intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "setup": ("https://setuptools.pypa.io/en/latest", None), } # ============================================================================== # Sphinx.Ext.AutoDoc # ============================================================================== # see: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration #autodoc_default_options = { # "private-members": True, # "special-members": True, # "inherited-members": True, # "exclude-members": "__weakref__" #} autodoc_class_signature = "separated" autodoc_member_order = "bysource" # alphabetical, groupwise, bysource autodoc_typehints = "both" #autoclass_content = "both" # ============================================================================== # Sphinx.Ext.ExtLinks # ============================================================================== extlinks = { "gh": (f"https://GitHub.com/%s", "gh:%s"), "ghissue": (f"https://GitHub.com/{githubNamespace}/{githubProject}/issues/%s", "issue #%s"), "ghpull": (f"https://GitHub.com/{githubNamespace}/{githubProject}/pull/%s", "pull request #%s"), "ghsrc": (f"https://GitHub.com/{githubNamespace}/{githubProject}/blob/main/%s", None), "wiki": (f"https://en.wikipedia.org/wiki/%s", None), } # ============================================================================== # Sphinx.Ext.Graphviz # ============================================================================== graphviz_output_format = "svg" # ============================================================================== # SphinxContrib.Mermaid # ============================================================================== mermaid_params = [ '--backgroundColor', 'transparent', ] mermaid_verbose = True # ============================================================================== # Sphinx.Ext.Inheritance_Diagram # ============================================================================== inheritance_node_attrs = { # "shape": "ellipse", # "fontsize": 14, # "height": 0.75, "color": "dodgerblue1", "style": "filled" } # ============================================================================== # Sphinx.Ext.ToDo # ============================================================================== # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True todo_link_only = True # ============================================================================== # sphinx-reports # ============================================================================== report_unittest_testsuites = { "src": { "name": f"{pythonProject}", "xml_report": "../report/unit/unittest.xml", } } report_codecov_packages = { "src": { "name": f"{pythonProject}", "json_report": "../report/coverage/coverage.json", "fail_below": 80, "levels": "default" } } report_doccov_packages = { "src": { "name": f"{pythonProject}", "directory": f"../{directoryName}", "fail_below": 80, "levels": "default" } } # ============================================================================== # Sphinx_Design # ============================================================================== # sd_fontawesome_latex = True # ============================================================================== # AutoAPI.Sphinx # ============================================================================== autoapi_modules = { f"{pythonProject}": { "template": "package", "output": pythonProject, "override": True } } for directory in [mod for mod in Path(f"../{directoryName}").iterdir() if mod.is_dir() and mod.name != "__pycache__"]: print(f"Adding module rule for '{pythonProject}.{directory.name}'") autoapi_modules[f"{pythonProject}.{directory.name}"] = { "template": "module", "output": pythonProject, "override": True } pyTooling-8.11.0/doc/coverage/000077500000000000000000000000001513317154500161325ustar00rootroot00000000000000pyTooling-8.11.0/doc/coverage/index.rst000066400000000000000000000005411513317154500177730ustar00rootroot00000000000000.. _CODECOV: Code Coverage Report #################### .. report:code-coverage:: :reportid: src ---------- Code coverage report generated with `pytest `__, `Coverage.py `__ and visualized by `sphinx-reports `__. pyTooling-8.11.0/doc/index.rst000066400000000000000000001171241513317154500162060ustar00rootroot00000000000000.. include:: shields.inc .. raw:: latex \part{Introduction} .. only:: html | |SHIELD:svg:pyTooling-github| |SHIELD:svg:pyTooling-src-license| |SHIELD:svg:pyTooling-ghp-doc| |SHIELD:svg:pyTooling-doc-license| | |SHIELD:svg:pyTooling-pypi-tag| |SHIELD:svg:pyTooling-pypi-status| |SHIELD:svg:pyTooling-pypi-python| | |SHIELD:svg:pyTooling-gha-test| |SHIELD:svg:pyTooling-lib-status| |SHIELD:svg:pyTooling-codacy-quality| |SHIELD:svg:pyTooling-codacy-coverage| |SHIELD:svg:pyTooling-codecov-coverage| .. Disabled shields: |SHIELD:svg:pyTooling-gitter| |SHIELD:svg:pyTooling-lib-dep| |SHIELD:svg:pyTooling-lib-rank| .. only:: latex |SHIELD:png:pyTooling-github| |SHIELD:png:pyTooling-src-license| |SHIELD:png:pyTooling-ghp-doc| |SHIELD:png:pyTooling-doc-license| |SHIELD:png:pyTooling-pypi-tag| |SHIELD:png:pyTooling-pypi-status| |SHIELD:png:pyTooling-pypi-python| |SHIELD:png:pyTooling-gha-test| |SHIELD:png:pyTooling-lib-status| |SHIELD:png:pyTooling-codacy-quality| |SHIELD:png:pyTooling-codacy-coverage| |SHIELD:png:pyTooling-codecov-coverage| .. Disabled shields: |SHIELD:svg:pyTooling-gitter| |SHIELD:png:pyTooling-lib-dep| |SHIELD:png:pyTooling-lib-rank| -------------------------------------------------------------------------------- pyTooling Documentation ####################### **pyTooling** is a powerful collection of arbitrary and useful (abstract) data models, lacking classes, decorators, a new performance boosting meta-class and enhanced exceptions. It also provides lots of helper functions e.g. to ease the handling of package descriptions or to unify multiple existing APIs into a single API. It's useful ‒ if not even essential ‒ for **any** Python-based project independent if it's a library, framework, CLI tool or just a "script". In addition, pyTooling provides a collection of `CI job templates for GitHub Actions `__. This drastically simplifies GHA-based CI pipelines for Python projects. Package Details *************** The following descriptions and code examples give peak onto pyTooling's highlights. But be ensured, there is more to explore, which can't be highlighted on the main landing page. Attributes ========== .. grid:: 2 .. grid-item:: :columns: 6 The :ref:`pyTooling.Attributes ` module offers the base implementation of `.NET-like attributes `__ realized with :term:`Python decorators `. The annotated and declarative data is stored as instances of :ref:`Attribute ` classes in an additional ``__pyattr__`` field per class, method or function. The annotation syntax (decorator syntax) allows users to attach any structured data to classes, methods or functions. In many cases, a user will derive a custom attribute from :ref:`Attribute ` and override the ``__init__`` method, so user-defined parameters can be accepted when the attribute is constructed. Later, classes, methods or functions can be searched for by querying the attribute class for attribute instance usage locations (see example to the right). Another option for class and method attributes is declaring a classes using pyTooling's :ref:`META/ExtendedType` meta-class. Here the class itself offers helper methods for discovering annotated methods. A :ref:`SimpleAttribute ` class is offered accepting any positional and keyword parameters. In a more advanced use case, users are encouraged to derive their own attribute class hierarchy from :ref:`Attribute `. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Function Attributes .. code-block:: Python from pyTooling.Attributes import Attribute class Command(Attribute): def __init__(self, cmd: str, help: str = "") -> None: pass class Flag(Attribute): def __init__(self, param: str, short: str = None, long: str = None, help: str = "") -> None: pass @Command(cmd="version", help="Print version information.") @Flag(param="verbose", short="-v", long="--verbose", help="Default handler.") def Handler(self, args) -> None: pass for function in Command.GetFunctions(): pass .. tab-item:: Method Attributes .. code-block:: Python from pyTooling.Attributes import Attribute from pyTooling.MetaClasses import ExtendedType class TestCase(Attribute): def __init__(self, name: str) -> None: pass class Program(metaclass=ExtendedType): @TestCase(name="Handler routine") def Handler(self, args) -> None: pass prog = Program() for method, attributes in prog.GetMethodsWithAttributes(predicate=TestCase): pass .. tab-item:: Class Attributes .. code-block:: Python from pyTooling.Attributes import Attribute from pyTooling.MetaClasses import ExtendedType class TestSuite(Attribute): def __init__(self, name: str) -> None: pass @TestSuite(name="Command line interface tests") class Program(metaclass=ExtendedType): def Handler(self, args) -> None: pass prog = Program() for testsuite in TestSuite.GetClasses(): pass ArgParse -------- .. grid:: 2 .. grid-item:: :columns: 6 Defining commands, arguments or flags for a command line argument parser like :mod:`argparse` is done imperatively. This means code executed in-order defines how the parser will accept inputs. Then more user-defined code is needed to dispatch the collected and type-converted arguments to handler routines. See an example to the right as "Traditional argparse". In contrast, :ref:`pyTooling.Attributes.ArgParse ` allows the definition of :ref:`commands `, :ref:`arguments ` or :ref:`flags ` as declarative code attached to handler routines using pyTooling's attributes. This allow a cleaner and more readable coding style. Also maintainability is improved, as arguments are defined using clear attribute names attached to the matching handler routine. Thus parser and handler code is not separated. If the command line interface uses many commands, handlers and their arguments can be spread across :ref:`mixin classes `. Later, the whole CLI is assembled by using multiple inheritance. In case handlers use shared argument sets, arguments can be :ref:`grouped ` and shared by defining grouping attributes. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Traditional ArgParse .. code-block:: Python class Program: def __init__(self) -> None: mainParser = argparse.ArgumentParser() mainParser.set_defaults(func=self.HandleDefault) mainParser.add_argument("-v", "--verbose") subParsers = mainParser.add_subparsers() newUserParser = subParsers.add_parser("new-user", help="Add a new user.") newUserParser.add_argument(dest="username", metaName="username", help="Name of the new user.") newUserParser.add_argument("--quota", dest="quota", help="Max usable disk space.") newUserParser.set_defaults(func=self.NewUserHandler) deleteUserParser = subParsers.add_parser("delete-user", help="Delete a user.") deleteUserParser.add_argument(dest="username", metaName="username", help="Name of the user.") deleteUserParser.add_argument("-f", "--force", dest="force", help="Ignore internal checks.") deleteUserParser.set_defaults(func=self.DeleteUserHandler) listUserParser = subParsers.add_parser("list-user", help="List all users.") listUserParser.set_defaults(func=self.ListUserHandler) def HandleDefault(self, args) -> None: pass def NewUserHandler(self, args) -> None: pass def DeleteUserHandler(self, args) -> None: pass def ListUserHandler(self, args) -> None: pass .. tab-item:: pyTooling.Attributes.ArgParse :selected: .. code-block:: Python class Program: @DefaultHandler() @FlagArgument(short="-v", long="--verbose", dest="verbose", help="Show verbose messages.") def HandleDefault(self, args) -> None: pass @CommandHandler("new-user", help="Add a new user.") @StringArgument(dest="username", metaName="username", help="Name of the new user.") @LongValuedFlag("--quota", dest="quota", help="Max usable disk space.") def NewUserHandler(self, args) -> None: pass @CommandHandler("delete-user", help="Delete a user.") @StringArgument(dest="username", metaName="username", help="Name of the user.") @FlagArgument(short="-f", long="--force", dest="force", help="Ignore internal checks.") def DeleteUserHandler(self, args) -> None: pass @CommandHandler("list-user", help="List all users.") def ListUserHandler(self, args) -> None: pass CLI Abstraction =============== .. grid:: 2 .. grid-item:: :columns: 6 :ref:`pyTooling.CLIAbstraction ` offers an abstraction layer for command line programs, so they can be used easily in Python. There is no need for manually assembling parameter lists or considering the order of parameters. All parameters like ``-v`` or ``--value=42`` are described using nested classes on a :ref:`Program ` class. Each nested class derived from predefined argument classes knows about the correct formatting pattern, character escaping, and if needed about necessary type conversions. Such an instance of a program can be converted to an argument list suitable for :class:`subprocess.Popen`. In stead of deriving from :ref:`Program `, abstracted command line tools can derive from :ref:`Executable ` which offers embedded :class:`~subprocess.Popen` behavior. .. grid-item:: :columns: 6 .. code-block:: Python class Git(Executable): def __new__(cls, *args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> self: cls._executableNames = { "Darwin": "git", "FreeBSD": "git", "Linux": "git", "Windows": "git.exe" } return super().__new__(cls) @CLIArgument() class FlagHelp(ShortFlag, name="h"): ... @CLIArgument() class FlagVersion(LongFlag, name="version"): ... @CLIArgument() class CommandHelp(CommandArgument, name="help"): ... @CLIArgument() class CommandCommit(CommandArgument, name="commit"): ... @CLIArgument() class ValueCommitMessage(ShortTupleFlag, name="m"): ... tool = Git() tool[tool.FlagVersion] = True tool.StartProcess() Common Helper Functions ======================= .. grid:: 2 .. grid-item:: :columns: 6 This is a set of useful :ref:`helper functions `: * :ref:`COMMON/Helper/firstElement`, :ref:`COMMON/Helper/lastElement` get the first/last element from an indexable. * :ref:`COMMON/Helper/firstItem`, :ref:`COMMON/Helper/lastItem` get the first/last item from an iterable. * :ref:`COMMON/Helper/firstKey`, :ref:`COMMON/Helper/firstValue`, :ref:`COMMON/Helper/firstPair` get the first key/value/pair from an ordered dictionary. * :ref:`COMMON/Helper/getsizeof` calculates the "real" size of a data structure. * :ref:`COMMON/Helper/isnestedclass` checks if a class is nested inside another class. * :ref:`COMMON/Helper/mergedicts` merges multiple dictionaries into a new dictionary. * :ref:`COMMON/Helper/zipdicts` iterate multiple dictionaries simultaneously. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: firstItem .. code-block:: Python def myFunction(condition: bool) -> Iterable: myList = [3, 21, 5, 7] if condition: return myList[0:2] else return myList[1:3] beginOfSequence = myFunction(True) first = firstItem(beginOfSequence) # 3 .. tab-item:: mergedicts .. code-block:: Python from pyTooling.Common import mergedicts dictA = {"a": 11, "b": 12} dictB = {"x": 21, "y": 22} for key, value in mergedicts(dictA, dictB): pass # ("a", 11) # ("b", 12) # ("x", 21) # ("y", 22) .. tab-item:: zipdicts :selected: .. code-block:: Python from pyTooling.Common import zipdicts dictA = {"a": 11, "b": 12, "c": 13} dictB = {"a": 21, "b": 22, "c": 23} for key, valueA, valueB in zipdicts(dictA, dictB): pass # ("a", 11, 21) # ("a", 12, 22) # ("a", 13, 23) Common Classes ============== .. grid:: 2 .. grid-item:: :columns: 6 * :ref:`Call-by-reference parameters `: Python doesn't provide *call-by-reference parameters* for simple types. |br| This behavior can be emulated with classes provided by the :mod:`pyTooling.CallByRef` module. * :ref:`Unified license names `: Setuptools, PyPI, and others have a varying understanding of license names. |br| The :mod:`pyTooling.Licensing` module provides :ref:`unified license names ` as well as license name mappings or translations. * :ref:`Unified platform and environment description `: Python has many ways in figuring out the current platform using APIs from ``sys``, ``platform``, ``os``, …. Unfortunately, none of the provided standard APIs offers a comprehensive answer. pyTooling provides a :ref:`CurrentPlatform ` singleton summarizing multiple platform APIs into a single class instance. * :ref:`Representations of version numbers `: While Python itself has a good versioning schema, there are no classes provided to abstract a version numbers. pyTooling provides such representations following semantic versioning (SemVer) and calendar versioning (CalVer) schemes. The implementation can parse many common formats and allows user defined formatting. In addition, versions can be compared with various operators including PIPs ``~=`` operator. * :ref:`Measuring execution times ` can be achieved by using a stopwatch implementation providing start, pause, resume, split and stop features. Internally, Python's *high resolution clock* is used. The stopwatch also provides a context manager, so it can be used in a ``with``-statement. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Licenses .. code-block:: Python pass .. tab-item:: Platform .. code-block:: Python from pytest import mark from unittest import TestCase from pyTooling.Common import CurrentPlatform class MyTests(TestCase): @mark.skipif(not CurrentPlatform.IsNativeWindows, reason="Skipped, if platform isn't native Windows.") def test_OnlyNativeWindows(self) -> None: pass @mark.skipif(not CurrentPlatform.IsMinGW64OnWindows, reason="Skipped, if platform isn't MinGW64.") def test_OnlyMinGW64(self) -> None: pass @mark.skipif(CurrentPlatform.IsPyPy, reason="getsizeof: not supported on PyPy") def test_ObjectSize(self) -> None: pass .. tab-item:: Version Classes :selected: .. code-block:: Python from pyTooling.Versioning import SemanticVersion, PythonVersion, CalendarVersion version = SemanticVersion("v2.5.4") version.Major version.Minor version.Patch if version >= "2.5": print(f"{version:%p%M.%m.%u}") # Python versioning from sys.version_info from pyTooling.Versioning import PythonVersion, CalendarVersion pythonVersion = PythonVersion.FromSysVersionInfo() # Calendar versioning from pyTooling.Versioning import CalendarVersion osvvmVersion = CalendarVersion.Parse("2024.07") .. tab-item:: Stopwatch .. code-block:: Python from pyTooling.Stopwatch import Stopwatch sw = Stopwatch("my name", preferPause=True) sw.Start() # do something sw.Pause() with sw: # do something sw.Resume() # do something sw.Stop() print(f"Start: {sw.StartTime}") print(f"Stop: {sw.StopTime}") print(f"Duration: {sw.Duration}") print(f"Activity: {sw.Activity}") print(f"Inactivity: {sw.Inactivity}") print("Splits:") for duration, activity in sw: print(f" {'running for' if activity else 'paused for '} {duration}") Configuration ============= .. grid:: 2 .. grid-item:: :columns: 6 Various file formats suitable for configuration information share the same features supporting: key-value pairs (dictionaries), sequences (lists), and simple types like string, integer and float. pyTooling provides an :ref:`abstract configuration file data model ` supporting these features. Moreover, concrete :ref:`configuration file format reader ` implementations are provided as well. * :ref:`JSON configuration reader ` for the JSON file format. * 🚧 :ref:`TOML configuration reader ` |rarr| To be implemented. * :ref:`YAML configuration reader ` for the YAML file format. * 🚧 :ref:`XML configuration reader ` |rarr| To be implemented. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: JSON .. code-block:: Python from pathlib import Path from pyTooling.Configuration.JSON import Configuration configFile = Path("config.json") config = Configuration(configFile) # Accessing root-level scalar value configFileFormatVersion = config["version"] # Accessing value in a sequence firstItemInList = config["list"][0] # Accessing first value in dictionary firstItemInDict = config["dict"]["key_1"] # Iterate simple list simpleList = config["list"] for item in simpleList: pass .. tab-item:: TOML .. todo:: Needs example code .. code-block:: Python pass .. tab-item:: YAML .. code-block:: Python from pathlib import Path from pyTooling.Configuration.YAML import Configuration configFile = Path("config.yml") config = Configuration(configFile) # Accessing root-level scalar value configFileFormatVersion = config["version"] # Accessing value in a sequence firstItemInList = config["list"][0] # Accessing first value in dictionary firstItemInDict = config["dict"]["key_1"] # Iterate simple list simpleList = config["list"] for item in simpleList: pass .. tab-item:: XML .. todo:: Needs example code .. code-block:: Python pass Data Structures =============== .. grid:: 2 .. grid-item:: :columns: 6 pyTooling also provides :ref:`fast and powerful data structures ` offering object-oriented APIs: * :ref:`Graph data structure ` |br| |rarr| A directed graph implementation using a :class:`~pyTooling.Graph.Vertex` and an :class:`~pyTooling.Graph.Edge` class. * :ref:`Tree data structure ` |br| |rarr| A fast and simple implementation using a single :class:`~pyTooling.Tree.Node` class. * :ref:`Doubly Linked List ` |br| |rarr| An object-oriented implementation using a :class:`~pyTooling.List.Node` and a :class:`~pyTooling.List.LinkedList` class. * :ref:`Path data structure ` |br| |rarr| To be documented. * :ref:`Finite State Machine data structure ` |br| |rarr| A data model for state machines using a :class:`~pyTooling.StateMachine.State` and a :class:`~pyTooling.StateMachine.Transition` class. .. #* :ref:`Scope data structure ` |br| |rarr| A fast and simple implementation using a single :class:`~pyTooling.Tree.Node` class. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Graph .. code-block:: Python from pyTooling.Graph import Graph, Vertex graph = Graph(name="myGraph") # Create new vertices and an edge between them vertex1 = Vertex(vertexID=1, graph=graph) vertex2 = Vertex(vertexID=2, value="2", graph=graph) edge12 = vertex1.EdgeToVertex(vertex2, edgeValue="1 -> 2", weight=15) # Create an edge to a new vertex edge2x = vertex2.EdgeToNewVertex(vertexID=3) vertex3 = edge2x.Destination # Create a link between two vertices link31 = vertex3.LinkToVertex(vertex1) .. tab-item:: Statemachine .. todo:: Needs example code .. code-block:: Python pass .. tab-item:: Tree .. code-block:: Python from pyTooling.Tree import Node # Create a new tree by creating a root node (no parent reference) root = Node(value="OSVVM Regression Tests") # Construct the tree top-down lib = Node(value="Utility Library", parent=root) # Another standalone node with unique ID (actually an independent tree) common = Node(nodeID=5, value="Common") # Construct bottom-up axi = Node(value="AXI") axiCommon = Node(value="AXI4 Common") axi.AddChild(axiCommon) # Group nodes and handover children at node creation time vcList = [common, axi] vcs = Node(value="Verification Components", parent=root, children=vcList) # Add multiple nodes at once axiProtocols = ( Node(value="AXI4-Stream"), Node(value="AXI4-Lite"), Node(value="AXI4") ) axi.AddChildren(axiProtocols) # Create another standalone node and attach it later to a tree. uart = Node(value="UART") uart.Parent = vcs .. tab-item:: Doubly Linked List .. code-block:: Python from pyTooling.List import Node # Create a new doubly linked list from an iterable node = Node(2) nodes = (Node(1), node, Node(3)) linkedList = LinkedList(nodes) # Add node before first element linkedList.InsertBeforeFirst(Node(0)) # Add node after last element linkedList.InsertAfterLast(Node(4)) # Get length linkedList.Count # alternatively: len(linkedList) # Delete node node.Remove() .. grid:: 3 .. grid-item:: Graph :columns: 4 .. mermaid:: :caption: A directed graph with backward-edges denoted by dotted vertex relations. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph LR A(A); B(B); C(C); D(D); E(E); F(F) ; G(G); H(H); I(I) A --> B --> E G --> F A --> C --> G --> H --> D D -.-> A D & F -.-> B I ---> E --> F --> D classDef node fill:#eee,stroke:#777,font-size:smaller; classDef node fill:#eee,stroke:#777,font-size:smaller; classDef node fill:#eee,stroke:#777,font-size:smaller; .. grid-item:: Statemachine :columns: 3 .. mermaid:: :caption: A statemachine graph. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD A(Idle); B(Check); C(Prepare); D(Read); E(Finished); F(Write) ; G(Retry); H(WriteWait); I(ReadWait) A:::mark1 --> B --> C --> F F --> H --> E:::cur B --> G --> B G -.-> A --> C D -.-> A C ---> D --> I --> E -.-> A classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; .. grid-item:: Tree :columns: 5 .. mermaid:: :caption: Root of the current node are marked in blue. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R:::mark1 --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; Decorators ========== .. grid:: 2 .. grid-item:: :columns: 6 * :ref:`META/Abstract` |br| If there is at least one *abstract method* in a class' definition, then the whole class is considered *abstract* and this class can't be instantiated. * :ref:`DECO/AbstractMethod`: Methods marked with :pycode:`@abstractmethod` are abstract and need to be overwritten in a derived class. |br| An *abstract method* might be called from the overwriting method. * :ref:`DECO/MustOverride`: Methods marked with :pycode:`@mustoverride` are abstract and need to be overridden in a derived class. |br| It's not allowed to call a *mustoverride method*. * :ref:`DECO/DataAccess` * :ref:`DECO/readonly`: Methods marked with :pycode:`@readonly` get transformed into a read-only property. * ⚠️BROKEN⚠️: Methods with :ref:`DECO/classproperty` decorator transform methods to class-properties. * :ref:`DECO/Documentation` * :ref:`DECO/export`: Register a given function or class as publicly accessible in a module. |br| Functions and classes exposed like this are also used by Sphinx extensions to (auto-)document public module members. * :ref:`DECO/InheritDocString`: The decorator copies the doc-string from a given base-class to the annotated method. * :ref:`DECO/Performance` * :ref:`DECO/slotted`: Classes marked with :pycode:`@slotted` get transformed into classes using ``__slots__``. |br| This is achieve by exchanging the meta-class to :class:`~pyTooling.MetaClasses.ExtendedType`. * :ref:`DECO/mixin`: Classes marked with :pycode:`@mixin` do not store their fields in ``__slots__``. |br| When such a :term:`mixin-class` is inherited by a class using slots, the fields of the mixin become slots. * :ref:`DECO/singleton`: Classes marked with :pycode:`@singleton` get transformed into singleton classes. |br| This is achieve by exchanging the meta-class to :class:`~pyTooling.MetaClasses.ExtendedType`. * :ref:`DECO/Misc` * :ref:`DECO/notimplemented`: This decorator replaces a callable (function or method) with a callable raising a :exc:`NotImplementedError`. The original code becomes unreachable. .. grid-item:: :columns: 6 .. todo:: Needs example code .. code-block:: Python pass Exceptions ========== * :exc:`~pyTooling.Exceptions.EnvironmentException` |br| ... is raised when an expected environment variable is missing. * :exc:`~pyTooling.Exceptions.PlatformNotSupportedException` |br| ... is raise if the platform is not supported. * :exc:`~pyTooling.Exceptions.NotConfiguredException` |br| ... is raise if the requested setting is not configured. Meta-Classes ============ pyTooling provides an :ref:`enhanced meta-class ` called :class:`~pyTooling.MetaClasses.ExtendedType` to replace the default meta-class :class:`type`. It combines features like using slots, abstract methods and creating singletons by applying a single meta-class. In comparison, Python's approach in to provide multiple specific meta-classes (see :mod:`abc`) that can't be combined e.g. to a singleton using slots. :ref:`ExtendedType ` allows to implement :ref:`slotted types `, :ref:`mixins `, :ref:`abstract and override methods ` and :ref:`singletons `, and combinations thereof. Exception messages in case of errors have been improved too. Slotted types significantly reduce the memory footprint by 4x and decrease the class field access time by 10..25%. While setting up slotted types needed a lot of manual coding, this is now fully automated by this meta-class. It assumes, annotated fields are going to be slots. Moreover, it also takes care deferred slots in multiple-inheritance scenarios by marking secondary base-classes as mixins. This defers slot creation until a mixin is inherited. .. grid:: 2 .. grid-item:: :columns: 6 :pycode:`class MyClass(metaclass=ExtendedType):` A class definition using the :class:`~pyTooling.MetaClasses.ExtendedType` meta-class. I can now implement :ref:`abstract methods ` using the decorators :ref:`DECO/AbstractMethod` or :ref:`DECO/MustOverride`. :pycode:`class MyClass(metaclass=ExtendedType, singleton=True):` A class defined with enabled :ref:`singleton ` behavior allows only a single instance of that class to exist. If another instance is going to be created, a previously cached instance of that class will be returned. :pycode:`class MyClass(metaclass=ExtendedType, slots=True):` A class defined with enabled :ref:`slots ` behavior stores instance fields in slots. The meta-class, translates all type-annotated fields in the class definition to slots. Slots allow a more efficient field storage and access compared to dynamically stored and accessed fields hosted in ``__dict__``. This improves the memory footprint as well as the field access performance of all class instances. This behavior is automatically inherited to all derived classes. :pycode:`class MyClass(metaclass=ExtendedType, slots=True, mixin=True):` A class defined with enabled :ref:`mixin ` behavior collects type-annotated instance fields so they can be added to slots in an inherited class. Thus, slots are not created for mixin-classes but deferred in the inheritance hierarchy. :pycode:`class MyClass(SlottedObject):` A class definition deriving from :class:`~pyTooling.MetaClasses.SlottedObject` will bring the slotted type behavior to that class and all its derived classes. .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: Singleton .. code-block:: Python class Application(metaclass=ExtendedType, singleton=True): _x: int def __init__(self) -> None: print("Instance of 'App1WithoutParameters' was created") self._x = 10 instance1 = Application() instance2 = Application() assert instance1 is instance2 .. tab-item:: Slotted Class .. code-block:: Python class Data(metaclass=ExtendedType, slots=True): _x: int _y: int = 12 def __init__(self, x: int) -> None: self._x = x data = Data(11) .. tab-item:: MixIn Class .. todo:: Needs example code .. code-block:: Python def Packaging ========= .. grid:: 2 .. grid-item:: :columns: 6 A set of helper functions to describe a Python package for setuptools. * Helper Functions: * :func:`pyTooling.Packaging.loadReadmeFile` |br| Load a ``README.md`` file from disk and provide the content as long description for setuptools. * :func:`pyTooling.Packaging.loadRequirementsFile` |br| Load a ``requirements.txt`` file from disk and provide the content for setuptools. * :func:`pyTooling.Packaging.extractVersionInformation` |br| Extract version information from Python source files and provide the data to setuptools. * Package Descriptions * :func:`pyTooling.Packaging.DescribePythonPackage` |br| tbd * :func:`pyTooling.Packaging.DescribePythonPackageHostedOnGitHub` |br| tbd .. grid-item:: :columns: 6 .. tab-set:: .. tab-item:: DescribePythonPackage .. code-block:: Python from setuptools import setup from pathlib import Path from pyTooling.Packaging import DescribePythonPackage pass .. tab-item:: DescribePythonPackageHostedOnGitHub :selected: .. code-block:: Python from setuptools import setup from pathlib import Path from pyTooling.Packaging import DescribePythonPackageHostedOnGitHub gitHubNamespace = "Paebbels" packageName = "pyVersioning" packageDirectory = packageName.replace(".", "/") packageInformationFile = Path(f"{packageDirectory}/__init__.py") setup( **DescribePythonPackageHostedOnGitHub( packageName=packageName, description="Write version information collected from (CI) environment for any programming language as source file.", gitHubNamespace=gitHubNamespace, sourceFileWithVersion=packageInformationFile, consoleScripts={ "pyVersioning": "pyVersioning.CLI:main", } ) ) Terminal ======== .. grid:: 2 .. grid-item:: :columns: 6 The :ref:`pyTooling.TerminalUI ` package offers a set of helpers to implement a text user interface (TUI) in a terminal. It's designed on the idea that command line programs emit one line of text per message. Each message can be categorized as normal text, warnings, errors, and many more. Therefore, this package offers a :ref:`LineTerminal ` implementation, derived from a basic :ref:`Terminal ` class. Of cause, it also includes colored outputs based on `colorama`. .. todo:: Terminal helpers. .. grid-item:: :columns: 6 .. todo:: Needs example code .. _CONTRIBUTORS: Contributors ************ * `Patrick Lehmann `__ (Maintainer) * `Sven Köhler `__ * `Unai Martinez-Corral `__ * `and more... `__ .. _LICENSE: License ******* .. only:: html This Python package (source code) is licensed under `Apache License 2.0 `__. |br| The accompanying documentation is licensed under `Creative Commons - Attribution 4.0 (CC-BY 4.0) `__. .. only:: latex This Python package (source code) is licensed under **Apache License 2.0**. |br| The accompanying documentation is licensed under **Creative Commons - Attribution 4.0 (CC-BY 4.0)**. .. toctree:: :caption: Overview :hidden: News Installation Dependency Tutorials/index .. raw:: latex \part{Main Documentation} .. toctree:: :caption: Attributes :hidden: Attributes/index Attributes/ArgParse .. toctree:: :caption: CLI Abstraction :hidden: CLIAbstraction/index CLIAbstraction/Program CLIAbstraction/Executable CLIAbstraction/Arguments .. toctree:: :caption: Common :hidden: Common/index Common/CallByRef Common/Licensing Common/Filesystem Common/Platform Common/Stopwatch Common/Versioning .. toctree:: :caption: Configuration :hidden: Configuration/index Configuration/FileFormats .. toctree:: :caption: Data Structures :hidden: DataStructures/index DataStructures/LinkedList DataStructures/Cartesian DataStructures/Graph DataStructures/Path/index DataStructures/StateMachine DataStructures/Tree .. toctree:: :caption: Decorators :hidden: Decorators .. toctree:: :caption: Exceptions and Warnings :hidden: Exceptions Warning/index .. toctree:: :caption: Meta Classes :hidden: MetaClasses .. toctree:: :caption: Packaging :hidden: Packaging .. toctree:: :caption: Terminal :hidden: Terminal/index .. raw:: latex \part{References and Reports} .. toctree:: :caption: References and Reports :hidden: Python Class Reference unittests/index coverage/index CodeCoverage Doc. Coverage Report Static Type Check Report ➚ .. raw:: latex \part{Appendix} .. toctree:: :caption: Appendix :hidden: License Doc-License Glossary genindex Python Module Index TODO pyTooling-8.11.0/doc/make.bat000066400000000000000000000014321513317154500157440ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=py -3.14 -m sphinx.cmd.build ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXOPTS=-v if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd pyTooling-8.11.0/doc/prolog.inc000066400000000000000000000026611513317154500163410ustar00rootroot00000000000000.. # Load pre-defined aliases and graphical characters like © from docutils # is used to denote the special path # \Lib\site-packages\docutils\parsers\rst\include .. include:: .. include:: .. # workaround for Python 3.11 not supporting backslash-space as escape sequence in doc-strings .. |degree| unicode:: U+00B0 :trim: .. # define a hard line break for HTML .. |br| raw:: html
.. # define horizontal line for HTML .. |hr| raw:: html
.. # define additional CSS based styles and ReST roles for HTML .. raw:: html .. role:: bolditalic :class: bolditalic .. role:: underline :class: underline .. role:: strike :class: strike .. role:: xlarge :class: xlarge .. role:: red :class: colorred .. role:: green :class: colorgreen .. role:: blue :class: colorblue .. role:: purple :class: colorpurple .. role:: deletion :class: colorred strike .. role:: addition :class: colorgreen .. role:: pycode(code) :language: python :class: highlight pyTooling-8.11.0/doc/requirements.txt000066400000000000000000000006211513317154500176220ustar00rootroot00000000000000-r ../requirements.txt colorama >= 0.4.6 ruamel.yaml ~= 0.18.0 setuptools >= 80.0 # Enforce latest version on ReadTheDocs sphinx ~= 8.2 docutils ~= 0.21.0 docutils_stubs ~= 0.0.22 # ReadTheDocs Theme sphinx_rtd_theme ~= 3.0 # Sphinx Extenstions sphinxcontrib-mermaid ~= 1.0 autoapi >= 2.0.1 sphinx_design ~= 0.6.0 sphinx-copybutton >= 0.5.0 sphinx_autodoc_typehints ~= 3.5 sphinx_reports ~= 0.9.0 pyTooling-8.11.0/doc/shields.inc000066400000000000000000000233371513317154500164750ustar00rootroot00000000000000.. # Use http://b64.io/ to encode any image to base64. Then replace `/` with # `%2F` and `+` with `%2B` (or use http://meyerweb.com/eric/tools/dencoder/). # Beware that `?logo=data:image/png;base64,` must also be converted to # percent encoding so that the URL is properly parsed. .. # Sourcecode link to GitHub .. |SHIELD:svg:pyTooling-github| image:: https://img.shields.io/badge/pyTooling-pyTooling-63bf7f?longCache=true&style=flat-square&longCache=true&logo=GitHub :alt: Sourcecode on GitHub :height: 22 :target: https://GitHub.com/pyTooling/pyTooling .. |SHIELD:png:pyTooling-github| image:: https://raster.shields.io/badge/pyTooling-pyTooling-63bf7f?longCache=true&style=flat-square&longCache=true&logo=GitHub :alt: Sourcecode on GitHub :height: 22 :target: https://GitHub.com/pyTooling/pyTooling .. # Sourcecode license .. |SHIELD:svg:pyTooling-src-license| image:: https://img.shields.io/pypi/l/pyTooling?longCache=true&style=flat-square&logo=Apache&label=code :alt: Code license :height: 22 :target: Code-License.html .. |SHIELD:png:pyTooling-src-license| image:: https://img.shields.io/pypi/l/pyTooling?longCache=true&style=flat-square&logo=Apache&label=code :alt: Code license :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/blob/main/LICENSE.md .. # GitHub tag .. |SHIELD:svg:pyTooling-tag| image:: https://img.shields.io/github/v/tag/pyTooling/pyTooling?longCache=true&style=flat-square&logo=GitHub&include_prereleases :alt: GitHub tag (latest SemVer incl. pre-release :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/tags .. |SHIELD:png:pyTooling-tag| image:: https://raster.shields.io/github/v/tag/pyTooling/pyTooling?longCache=true&style=flat-square&logo=GitHub&include_prereleases :alt: GitHub tag (latest SemVer incl. pre-release :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/tags .. # GitHub release date .. |SHIELD:svg:pyTooling-date| image:: https://img.shields.io/github/release-date/pyTooling/pyTooling?longCache=true&style=flat-square&logo=GitHub :alt: GitHub release date :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/releases .. |SHIELD:png:pyTooling-date| image:: https://raster.shields.io/github/release-date/pyTooling/pyTooling?longCache=true&style=flat-square&logo=GitHub :alt: GitHub release date :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/releases .. # GitHub/Libraries dependent projects .. |SHIELD:svg:pyTooling-lib-dep| image:: https://img.shields.io/librariesio/dependent-repos/pypi/pyTooling?longCache=true&style=flat-square&logo=Libraries.io&logoColor=fff :alt: Dependent repos (via libraries.io) :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/network/dependents .. |SHIELD:png:pyTooling-lib-dep| image:: https://raster.shields.io/librariesio/dependent-repos/pypi/pyTooling?longCache=true&style=flat-square&logo=Libraries.io&logoColor=fff :alt: Dependent repos (via libraries.io) :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/network/dependents .. # GHA test and coverage .. |SHIELD:svg:pyTooling-gha-test| image:: https://img.shields.io/github/actions/workflow/status/pyTooling/pyTooling/Pipeline.yml?branch=main&longCache=true&style=flat-square&label=Build%20and%20Test&logo=GitHub%20Actions&logoColor=FFFFFF :alt: GitHub Workflow - Build and Test Status :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/actions/workflows/Pipeline.yml .. |SHIELD:png:pyTooling-gha-test| image:: https://raster.shields.io/github/actions/workflow/status/pyTooling/pyTooling/Pipeline.yml?branch=main&longCache=true&style=flat-square&label=Build%20and%20Test&logo=GitHub%20Actions&logoColor=FFFFFF :alt: GitHub Workflow - Build and Test Status :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/actions/workflows/Pipeline.yml .. # Codacy - quality .. |SHIELD:svg:pyTooling-codacy-quality| image:: https://img.shields.io/codacy/grade/08ef744c0b70490289712b02a7a4cebe?longCache=true&style=flat-square&logo=codacy :alt: Codacy - Quality :height: 22 :target: https://www.codacy.com/gh/pyTooling/pyTooling .. |SHIELD:png:pyTooling-codacy-quality| image:: https://raster.shields.io/codacy/grade/08ef744c0b70490289712b02a7a4cebe?longCache=true&style=flat-square&logo=codacy :alt: Codacy - Quality :height: 22 :target: https://www.codacy.com/gh/pyTooling/pyTooling .. # Codacy - coverage .. |SHIELD:svg:pyTooling-codacy-coverage| image:: https://img.shields.io/codacy/coverage/08ef744c0b70490289712b02a7a4cebe?longCache=true&style=flat-square&logo=codacy :alt: Codacy - Line Coverage :height: 22 :target: https://www.codacy.com/gh/pyTooling/pyTooling .. |SHIELD:png:pyTooling-codacy-coverage| image:: https://raster.shields.io/codacy/coverage/08ef744c0b70490289712b02a7a4cebe?longCache=true&style=flat-square&logo=codacy :alt: Codacy - Line Coverage :height: 22 :target: https://www.codacy.com/gh/pyTooling/pyTooling .. # Codecov - coverage .. |SHIELD:svg:pyTooling-codecov-coverage| image:: https://img.shields.io/codecov/c/github/pyTooling/pyTooling?longCache=true&style=flat-square&logo=Codecov :alt: Codecov - Branch Coverage :height: 22 :target: https://codecov.io/gh/pyTooling/pyTooling .. |SHIELD:png:pyTooling-codecov-coverage| image:: https://raster.shields.io/codecov/c/github/pyTooling/pyTooling?longCache=true&style=flat-square&logo=Codecov :alt: Codecov - Branch Coverage :height: 22 :target: https://codecov.io/gh/pyTooling/pyTooling .. # Libraries - source rank .. |SHIELD:svg:pyTooling-lib-rank| image:: https://img.shields.io/librariesio/sourcerank/pypi/pyTooling?longCache=true&style=flat-square&logo=Libraries.io&logoColor=fff :alt: Libraries.io SourceRank :height: 22 :target: https://libraries.io/github/pyTooling/pyTooling/sourcerank .. |SHIELD:png:pyTooling-lib-rank| image:: https://raster.shields.io/librariesio/sourcerank/pypi/pyTooling?longCache=true&style=flat-square&logo=Libraries.io&logoColor=fff :alt: Libraries.io SourceRank :height: 22 :target: https://libraries.io/github/pyTooling/pyTooling/sourcerank .. # PyPI tag .. |SHIELD:svg:pyTooling-pypi-tag| image:: https://img.shields.io/pypi/v/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072 :alt: PyPI - Tag :height: 22 :target: https://pypi.org/project/pyTooling/ .. |SHIELD:png:pyTooling-pypi-tag| image:: https://raster.shields.io/pypi/v/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072 :alt: PyPI - Tag :height: 22 :target: https://pypi.org/project/pyTooling/ .. # PyPI project status .. |SHIELD:svg:pyTooling-pypi-status| image:: https://img.shields.io/pypi/status/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072 :alt: PyPI - Status :height: 22 .. |SHIELD:png:pyTooling-pypi-status| image:: https://raster.shields.io/pypi/status/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072 :alt: PyPI - Status :height: 22 .. # PyPI Python versions .. |SHIELD:svg:pyTooling-pypi-python| image:: https://img.shields.io/pypi/pyversions/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072 :alt: PyPI - Python Version :height: 22 .. |SHIELD:png:pyTooling-pypi-python| image:: https://raster.shields.io/pypi/pyversions/pyTooling?longCache=true&style=flat-square&logo=PyPI&logoColor=FBE072 :alt: PyPI - Python Version :height: 22 .. # Libraries - status .. |SHIELD:svg:pyTooling-lib-status| image:: https://img.shields.io/librariesio/release/pypi/pyTooling?longCache=true&style=flat-square&logo=Libraries.io&logoColor=fff :alt: Libraries.io status for latest release :height: 22 :target: https://libraries.io/github/pyTooling/pyTooling .. |SHIELD:png:pyTooling-lib-status| image:: https://raster.shields.io/librariesio/release/pypi/pyTooling?longCache=true&style=flat-square&logo=Libraries.io&logoColor=fff :alt: Libraries.io status for latest release :height: 22 :target: https://libraries.io/github/pyTooling/pyTooling .. # Documentation license .. |SHIELD:svg:pyTooling-doc-license| image:: https://img.shields.io/badge/doc-CC--BY%204.0-green?longCache=true&style=flat-square&logo=CreativeCommons&logoColor=fff :alt: Documentation License :height: 22 :target: License.html .. |SHIELD:png:pyTooling-doc-license| image:: https://raster.shields.io/badge/doc-CC--BY%204.0-green?longCache=true&style=flat-square&logo=CreativeCommons&logoColor=fff :alt: Documentation License :height: 22 :target: https://GitHub.com/pyTooling/pyTooling/blob/main/doc/License.rst .. # GHPages - read now .. |SHIELD:svg:pyTooling-ghp-doc| image:: https://img.shields.io/website?longCache=true&style=flat-square&label=pyTooling.github.io%2FpyTooling&logo=GitHub&logoColor=fff&up_color=blueviolet&up_message=Read%20now%20%E2%9E%9A&url=https%3A%2F%2FpyTooling.github.io%2FpyTooling%2Findex.html :alt: Documentation - Read Now! :height: 22 :target: https://pyTooling.github.io/pyTooling/ .. |SHIELD:png:pyTooling-ghp-doc| image:: https://raster.shields.io/website?longCache=true&style=flat-square&label=pyTooling.github.io%2FpyTooling&logo=GitHub&logoColor=fff&up_color=blueviolet&up_message=Read%20now%20%E2%9E%9A&url=https%3A%2F%2FpyTooling.github.io%2FpyTooling%2Findex.html :alt: Documentation - Read Now! :height: 22 :target: https://pyTooling.github.io/pyTooling/ .. # Gitter .. |SHIELD:svg:pyTooling-gitter| image:: https://img.shields.io/badge/chat-on%20gitter-4db797?longCache=true&style=flat-square&logo=gitter&logoColor=e8ecef :alt: Documentation License :height: 22 :target: https://gitter.im/hdl/community .. |SHIELD:png:pyTooling-gitter| image:: https://raster.shields.io/badge/chat-on%20gitter-4db797?longCache=true&style=flat-square&logo=gitter&logoColor=e8ecef :alt: Documentation License :height: 22 :target: https://gitter.im/hdl/community pyTooling-8.11.0/doc/typing/000077500000000000000000000000001513317154500156515ustar00rootroot00000000000000pyTooling-8.11.0/doc/typing/index.rst000066400000000000000000000002041513317154500175060ustar00rootroot00000000000000Static Type Checking Report ########################### *Placeholder for the Static Type Checking report generated with* ``mypy``. pyTooling-8.11.0/doc/unittests/000077500000000000000000000000001513317154500164015ustar00rootroot00000000000000pyTooling-8.11.0/doc/unittests/index.rst000066400000000000000000000005051513317154500202420ustar00rootroot00000000000000Unittest Summary Report ####################### .. report:unittest-summary:: :reportid: src :show-testcases: not-passed :no-assertions: ---------- Unittest report generated with `pytest `__ and visualized by `sphinx-reports `__. pyTooling-8.11.0/logo/000077500000000000000000000000001513317154500145325ustar00rootroot00000000000000pyTooling-8.11.0/logo/icon.png000066400000000000000000001255111513317154500161750ustar00rootroot00000000000000PNG  IHDRFl]zTXtRaw profile type exifxڭu9maEy0Z3A$%HTK"@ uTjn9[Pl?XzFwy=@M.zAw֭k ^q^ko{,c%^ \_)]xR9~5Uz= suߍ9Bp۾.~z~lW~=ۣܟ8g|&~.Um/u;9Oďnt)F}o~dD} -P$s{ߦqʝcw\̾2|O>\s/_Fw^1M7IN7)?//$"LTIV)fRBݤbJ)jjcN9璅SK*RK+kZkdZnZܴε:;/ ?ˆ#<ʨ>)gyYg}`V^eVnSJ;.N8O9dՏYg~5ʚҸ3k\%$)gdGGƋ2@A{VW3|0!$,2Fv>#w?3m y_e(u?2gw/p.$ dNq6|-qv=>vK5.kӴdn@+rV ( N\ax}>h߆?\6?g<mj.Thcxyiuv|⚣MgFwu6Ȕiih&ld_ӪeSa0ώU= XTn4tsC≳աq,ZMv[O9|K="> y" O7pʍy6uovBujYmĤ="Ҏg 40tbm(s`(7FV:W9]smG{P 6r :$;Z UR~Һg짵W+d7mN(8LR9Lg BQ@Q03YE1@{p43Ϝ_ X_jYkfǚo6mƚ {z6c;7lv:->>USE jet*]3X[>5&o4hK'`:be*;6+I-OeNt(k\+|\1"茝<֞k.\*Or/M LcR{G'L=ÊoI6Z 9dvmgԔ߳9 ]{ºP`O+qi-M)(EjL@4JٳCpns3׍g۵|fH_?vFUr\YDN.M.քwbꈧsЍdDŽ$+xPPvĐ{DT u њB9u}!ӽe;n ?Wn+cj?!C (WrqO]pGz̚zXw+U!,J(Af*ƊLHe2 Cȣ`4g;qT* EQAᶢ>&_tZ R'A!kȣ_~T\fFKWZoy6# f]M 5e⠽F}R#.-5VF&5bW]Q|nHg ]I g̹S+C3Lw75ĥTǂ{Wn&G}I?^{qY(F;6&KR$;۞t]sṬʜUfF!JtrJ7ItmۣoI5&1&hQ 8\(qE1vP\d4`h@&Mma6.S7T! Фif;'4.7M594qjaEW9K̙pr՝ Vzbn-l~5>ՙuN~MrY9%6q)-l=qí팙j>BSDm'vTDKBPoÐqk6=7)Iı,D'Ql3Jl'p%B/S|M6fk@PܙTc 9 љF)D X@݈Y) #!׃8%e h@~!põof.u4`\dZ*ŭA$ۏ8PRTZw%cѬnGUT7i3T'cE;R4 o2FAډ 9YW t鶀9W|.iPPxZ꘵FIC:Y1P%NXԟVvITE/y7Uw?Вw*:( I' G͍E}@y"yAl*;Tu-bvr)KLbL- S3ܧB>a;)*!K:cN &$'hБ5о XW* Fw!FZjyc2N$7w0a3VbvTA&ff4jdt%aHCRh}@`tP!\P@ĝ K#+. o7@a'OgP#B" [M< 9`0/4hedzG,PliZ^,QWn<(PEriPIP`i[7ye%fTiӽ)HRHcרյ RФ.7SK;*kڲ>aJ}f& ;dAcó޽Y4!9-|Af"kg63$^)&˃iC: iAQGMh"qI)g.r:k&>ݤ)b׺U9έ(߁5J߅oVS,mH;b%?R-ZFQip Q4_@Q?p-K ;ݍAڽl(ǧK"_8i]2 & Er?vjwL%JQ0ܸ%z]|Ǡ[%RԲtv>WDGE5kC+C%<Ƚl~H'Åx h CIEs(lASG`vR-  upk i-:)$GN! 6>gid{qoEcᠷR{pZ`T4S΁VQJ_` mJ a}~d'Ok XpIlbE %E+.s"" Nsd&*)HC-z"1}7pjqH:u%Z⚒;W$o9 ))PU6($ (2q@׾4ۉ%l/V*3مsP a>@%@0:'%ɊZAJՕ&4Z)X MH ;iRT:jM S4 ~Y{mnbRo{;\G$Y:@ƁJxE-T}RٴEkSBFTkm(H%G)ew-UZA)!5Jc; 7x,@N OCpMCwY&d`IHZ^PMR5џ* 0O;ashB~P0ZZni6% /hdLuC!L)\(pu#k6=NjZ91?z!U¤u`RSҮh AB"z3Ь-W8D  oQُ RF؆+}L{ȩU/C<] ץ&h~9)Tml&~s#Ç,qڂjԧ0>3\? SQ!TK3)n8Mt.l]ǁ;:NI׊SЩZ~WMw!?#UWJWijt]TqX5-ЇH1ĠDpu~a?gZDw. P6ĊCpmKyj95IO9Ԫ@xƙ8y"VTWpՒ{E 9les+1 aӂFkIH7ڠ!B0/R]^2$hqD:p] aUZ L \H? B & ,AI@wňZAΞ«ke7Z< 's_Y ~WCHr\YFMǠc!ȇ3 Ɯ-YsC3'KNv9:+7u>;D.gUvkEXQH[Bh% Ļ(ayd`D-Ӣ漌nLu]!= )+ 9D@(Zע4%rHA4bx{Q|8=ma6H74 VcFmil6ZԣHV㴽آxX$['A{g*цM0'H:K`C ޅ$$ !赗sBSD)w @D1KGwyW쏒ӳu/IkWi=><2YN-o.ƃ|MN^-ʝ qy:z:quW8kuNk# -k\B#ݮ6 &m\r"昐bpʽR3m߻'.U-9\?%סmvV b@%C#yai@HeJ/qtun ۃgC>e]2U;RH 5 "\uikY ]}Je'tFk@Ѷ6sA52JA:SX)K '1-`RrLV;)ύ!_p#1<ŀDXRmPg8WgDHx MzS=ן-og NZ6 KY2V MCxCPV2nHw -K|twi/'~78Sv kL$/8`)Xuu C/bϭJ\CCm@!a4K']Gec6n.~POXtt 2FyP >Ls6;xlZQ(/α BCO}t.?mm/|>Mo٦GhSxOnͤhmwKaW{?>$'dv$r$ߧ4Q]h%PGZ+bavZb:%AqC~[g~.z@|_'QFU mw,*:a/O>RWKCyQ_m9^R0cvv0$RdkӟDGQQOGl;*ڼ_ZJz΂-V)]:q($c'#_cIm+3!=$`k}&bqƆS]rB7ywzs\'MF7o oh\3~t񷿛?9kкΑWAofԡ z".6ŀ*m/w?a" yY!B%iJ"{lu3UBhO*xӌ"E8Nx9#]21r%+2N+pY%~E,;y垅O (QGf(P bstrm=юd־O&rvO^!}<#5VI`NQRZ ߵ}]xBL'r ?߼=@=RQ'}4Ă{E42V].oyx;2TC4w<_c(?c`ƯP9"MXYUi~*M$~~,"CVΰ#CFGS Na&H`b/|?">97iCCPICC profilex}=HPOS"- !CdATQX ЪK!Iqq\ ,V\uupG''E)" q=Vf8jM%BqE ‡ˆ".1SO{ꦺK,?+LDY:s'tAG.q8,̈GJ=̪J8599L&i%I\.?~\{ر-G4˗/ĉZ^YQ\ԤFFF }V\Ғ~i500:MScd߯WwAj۷3N8o|ıqm߾ #WF_|Ej5}{511e56GVSO=o:|z{{}H<5??lsN/\ܐ`}VJE۷o$eXkN8!co߮J"I|9'NhVS&5>>k&\cua=䓚zC+[@0 +H["[j;v:OhllLtI}+_ʊ>x5wHQirrR333D1F\N :vVWW%IΝ&&&Z6bMUih\2HE7%X>`_~'t1?/|fffZ -ꫯqed\azk14Mo{+ƴ_~Y I~Xccc-քk_=j.\O|zOa^ګ7=<'OԹs433/| m!ΝS6UEt__ݔv {9箶8箫o{W^\?~XRƿWrVr$\-cDZΝ;GJ W^,irrT|lVrVr$\র֪\.U,z)~7$\ʋWF(c=ǏJklcss @w}0TPСCtaq`wZyRhvvZ+9 xWZG+;З%i}}]aT*s6 ȑ#?cʋﶒȈFFFkz1K###Z^^ֱcͫ$=K]I9Yl5@ \kp p o09*`K8v\p 5@@ \ \kp p 5@@ \ \kp p 5@@ \ \kp p 5@@ \kp p 5@@ \kp p 5@@ \kp p 5@J64vCґ,iGF53Uك0ܙDI=ÑTǟ[THc Z9Nf5y[oN7甬&J #I>IIlrb;U-&;)j`"V*mxyoF^PI=Oq 0׽$$/KIK B4IU_KeUYkK׍Mc:Ḙ?9Rǎ9Gnw&4GF %#x)̆ "02Vs>7W^j$Umy]jV4v]_WlHkݏ(J=R5* g9J2l{/I^ijS)i?tT]. ʷȟec \6WЗip:\ѨЛQy8/c F֙}T_7ZsZkhq^|jM/5>\B#lAwCefU+_vrF^ [c(c#qVRE#U"5^RC,jĊw^'n@` `3)ߛԭeŅH9W[y"lɅV.bJU o/itWI\ڊYK<Si!gTs 3NqνO.FA%PXTy8FMݷ/^Śyk \@ױ@{2:P]B_V^F֤j Hz" fPuK:-jTB \V4E 7+WʘTZO1^A${#yB.V5s^'[Tm~]jU@-4j`OIc*^Te0V\dM"啫_ՌwUԨ5tWϩ^] d \@g VSw5uي*9QsԵ EdقUFžHќ.Xӓ p 8m:۫#qc//cJ}.48]xqI_ZSζvrVXccb[hq.t{_W}l`+bp&F3U^Rzye kT@&)o\њ+l'!\Mdi@N) l9O?u۹2a6~=S: Z9 pg5~/#B7rXó4o1]@azu/R`IIFJ펪zWOz^2@xn۟=?3) [FOWN?@Q !zn)ύj#Vo%XE4-SBkhΜnԘ&o):DlQxUczlTќ?z5`SS@*29+kRr]<׏yA?W%I&0 p n׳uOrf6JbDYy"/L y&kwM{P&#7զ0]Y`rFY9}:Na5ZdC 0'Tw9#/g*>2*w[A=65)N xE72:q5S}wNSp x+VcÚ>XbѝR}5w~'тӶ{c˚۔4)@dBPn*}N`t?Ux/'LK&"İn}tLٜ1#B畮s˔fb|bT>4"k/)Yo3liJ׮0gǴ!56 xbBw>9D15tGTAhtcG(@ώh= -n'`knvp@`U"Ȱ ~O{THt{!}} ` RT`<վGuP p /7ȎVxnydX Mi|_YAH-p2- Sp ~( `1[:]Ŵv30C+$_g$vaȈ2/C96K5 \@ˍE:ؤTh/Φ y S }gsZD{2 Tӷt'iЦ*o);v׫ekh3@?5A#"qU;-@v9 Fwt #:nZΟ-9j=LWG. λTP#+ J_kF*/1Y3N~O;c*Z :Ӈyf>nݟr Yc (YQP )@k)p DV4zH15XrD5cz"mP *C!&V-%J15>ҫ%P lDzzr#@h*[b[1^S9Mo&*S*3Cbi궒 \@+e d2FwUC \3ѾG?]F"*e \&b_FHZ[TGs>A!5lYhAcFvV(@3o)+wȈbff \&QT&ۜmP{gQp ?j@YrH1Еh`2ĀÑ,g:t\O>ɋއ yߐbQxD!5wSʗ,3L[~Bk~($E@wSp NAl,:Ap *\@Ci^p&2 N;3 \>Ȓ7^lBkv#{+:ZB \5(YFU\@w`QugASp q"Ii" \;@ܖW0B[W?dH{rʕY8xQ#էc \ N.2>;kkt3~RPpwTd,-!;J@IB%5MXk \0XWe4#skxh5;lSpMx|e55:m,:$Vyh@ձ"Q7TL 6#:;\Y+Tqkc|O0t\W0^Ade \q2ڌvFQ.p$fLV2bɼ)H{oRo=!-=ݞ&M-)r*h 5ͱR'FJyQi(0n>n&I;*gu% t=g(&\4+uz-%jzkj%Ֆ|*R@ BP w$yb)S~b7,=U$˓n|e4zY/|{^_Y֩gT_Mv_{Gk:%)M]i>VQw# \$5 +PIC?;/7[;Ac1ՙ43K:&4|9]"75@2op lqE tXӇ)2䏖{oGtN#zOkRB0Fa>RT"k+W TŔo7BC/=3oۓZ~e%S}V'-% DͭRp N׼=/VYyhHRD[*U)S%9SG")xCn%ν7o#pQ m+ɅV%5I"X<}{yI'u7uT'ȮF(@05wzUiWn'8D BL@wG?gdVR~aE/}k}o:l>y]>SkJk<"<[hu~].jM_U2P!k0ְHxox9ѥ7b_\[e+J> t\OF.4 VZиůcU^(ap N602ZbN=;nȵ s[љ(JF> \ZA*ʅ2\|MsGo<~c=ׅP^BZ!IRϋ@wTkW`Ӄ'm` l%E[\^tD\FVV\ҥ:A&r6- \fa)(%ZFC$g%޺k͔O+@V/׵xa0TVOŐӢpz3p ]0aKF2jzU[p Ņ@J$A VsQ6D.$ZYhMdM @͒++4;򾗘Zd}5rBu+IzBʼn7EIjz4@Z{klVM>ҼuRk].YG(hIdL[!2|mӂhmm*k,3[k] p€N-7a*ʐZ=O5hu!zp[qۮ:St\Zqzkp #A2}D v{M,vi͔ @2@ŞB@R ׵TˍκX檜tkݫQKs-/cѰ]佑OM3{ګ5V[JXgVke|Vo4fKncvquY6I8z5n*,܊*Mۧiw0~cxɴJ5[Bl5@͘P^VֵOܺtӵ9ҍ˫1򍵉IdWըd_䓶g8RH7'(MW\KIꔤk]m})=- LSf0j$JwP|RFDtlũHuk7rm/&ZtP ;m(pp,k,˜SXKL3Q׍q3ziy:@/5vV\L*u6o逩MdKS\8k`S@'r[jk1Fې1#F6Q6|E t$V-o$ڪae!R/kKw­3KQYHUucN 9StM7@ת-Z]lHp>a+_vd"P)M,/5k]}rFΙG U[S^p y\9Zʫ],Z^`:gdM1C 7@FkduhQe@\Z˫ ].xCYe 2qF6/:_?˚$OSTz[TRtHVZшbl͙.h8kFVD4JRl IDAT˜7,@de:p2(G(*ӓS #22HJ`^>JT>֗VrnI+gT_SZOTT;tGF.d5q($PJR1xٷR7S[8#xgb'RR*V%8E%K Uai})ʙNڥD.2 V5bb6? \pVQOV.v rETsr@Z 5.*̅A +eNo)#2U5&U_]WRk'6/մz~I˧vaEՆά(]jQL)W֢to>Ivm9uTalIr;Ӈ(\6\\ʅF; 2Vi#UfTa)Sd \ 笒F4iGՖjU-' 汱(ixW.7T[Hv1 $%p.žX6b).dC+ X9ɾü9o ^_ F |'u՗T_^WROZ>K?:sKJ5.+!lo뫩i[r~5_UǿvjN;5GӆWNk9=ŒUy0jdwUIqV2)]`dlisIߜ+iHF{\k}5U\ns0:I ϯ0hB7p5:b\1qVPN]dCHEYk_ ŌB?z5fU֯BdҊ_X+]KrTzzגs2&;Wr68ik4_^NXj$ɪw HAhrs*w% 3K*+dQJ Gk -ԕ^ZsGtS5TZ>ېo 6D;4lA6pTUi|h&Hw .o5 A-Wc\k|<$57L>m!TlM6i\U0oU̪:Qu)ԗQKb8(2VJTڂ8a]ݧ[~uFֽH{Kij^-[URO%xn]I#њkV/6uO&\mNȕBTLo^N(*2ּuKV˖aeBTꓼW|涟Қ_\RvGK6&p5HեT+Kd6}kU9o sir{˪e-* *fT睂*8;6p{59k^=3ū_e0+.Ź5媡._"| :5n WUVQn9ݳ+ٍ-3lЩgzf499hȜj'ncIݫ^Kwx}BUb50foԻf[M:PC3*48W/Vy(8Bۜʤ2uo6WiN':!_m2V:˚W/􏗴zu13 -gNѼ➬U5`vn]o(My~Z~e^uFL}-JC}Wghn٩_yƝr#VN'K*̩ЗQur)}#+#[3rh6}OZE_ӥӫN=ϯF ڄklz3uHcwoSiG&o 9J:wGtZ>6uBv(TʽV:a(Mm[=^_]JT[M˳}69ɚDnfQF}rkׇU U* u6{+9zc m/i̪wpnMg_u5x':ko@Fӟ٫K؜@;5YuF n8k+L+4@ZZ4ʣ8k߲E k^NU*Sp22+;I;YPe$W4zUOV\׸P=ծ/ݡ{ 1O7^ԫ"WcHkFkHݭgV%X X'>PMKNq)t +Ny[|Z]$8*zFrZߓ̑ .j욎}gA^\Xkުae{ ܸKőv=qPSב{m( 3mt$cj͑93\YfoOF#V~pNƨ9JM޼4rU9MQ}-ӫ*dtG<"\׎nUëse⦦l)W};4tpB?/O)].,Uqn]jsrzMrY6fkg9xTΨ2i|I*ضz1v+S9Si}p|IJgEY'՗ \wLiCb,m=ٸQ7zp|NߒF˴l2m5+Wm5Up݂7 ^Fo+hFͨw(h Uhaf4ReKH; ꪞ Z9˪n9צ@*j[U4秶%e9s´PxĴ褔ʚM_T U+pw$T#qJs=yI{,:7Ul}I n+2LMՑ~ghyz [Ԗxgd*3* _{كTg@^eyUtGu/^0:v8>Zܓ4<h[776g-mwVaY'19ݶ5F&2@.Z?D[6 >x\c .fR7]:~/IǍm|Jȫ}p1\j {SoPe؆Bא۟{*:j`U=t댊ezFs,(9v:ugOw߈26P~\Qq)uk/i hV>͕ e?o$m.MǍXEE}ԡUGr䚁 u`+\N5zU7ẃ@U>دDUPa ̖F}GԳm@N?uT{Vr(PZ_M-cYs>#Za,YDZҊ)msEηohւ&W?]`UԎ?(T̪P2_,tK0hᝪLŔ()F+:7sBgV^Z01m!-?iv񶖑~chE{54[R3*iB+ZE@q!'K߾ck:4OX םN[󇔭Z+T.^$)gdhkQ|D*=כlCΥoۧ=2NORjtoUVl=g,@Cnc%Ujc(*f(d@P?Okؒ;%em9B`k5G2&o[1rFi" *2Vxɾ[DY (_ UbV.:!\ު6'd k1[~nw)Yek^V5;m4a&g^>J[zDۊgThN݆bQ 8L1ЅWVtKp{QЀvr^asF>;t~I<# @\E:FZ%m&VRkF16dmٜrX=yYt9ZJvB.i+yobQ;-0wIRiey^#~7 2ؒ博}sj4F+fl (UxVެ-AT | m+髿r\K(*j瓇4>eָշkHT[ "{5hToI2J}-XerP7JOIY/1)M#¬Ӷ;NQ7V]C>4 jg@~ w1jRV њQF0-WM<%SZ51iK3w 7C~Ugdv5*T}lV&0}+Q*sݢlݜBV릋6Akr7j4xB\'6~tFӏSi" WԶOUPf7k\ ZtfKiCJ njިZ+9mU `اkûU6!XcŕOߢh(KA^ju?[SYU@5d[^7&i;ʹ6M;U*h -9Q)CS[V$W‚vm7NF^Ƥ]R~5o6^rMnCޮnSaHF+1  6OSawH Ufmۍ_ms^͗ I69ohlM ׸j⳻4JbBP>_7v5oLӟڧm}]_$BZ¼vQ[9*m7RMpݖߩv*?Pj dCU5iRVx!E{FurS][!9e ׆k j3|x<#ָ)[Łzwkc{^z$T\JᒆߴN1ڴ- `&cL̀9ܢD+K-*E-֩l[-൲]O*IL"/)sMBz֎oSoNA2vkt_4VzЪ]lv%Z˶Q?؄rh@;?wPٞ|smd#u+9.%|v學0T&r}FܰHyYH7+؄NVW;>{@2#hÀ a.퓚ޮEB%M%nFIbSܴo ڪU `KaOU3E^]D'!Hn'f2VqSXkی|\_N"~MH&>SG(:% )*d3wϨ-LTˋҒ 1Kk(#n6S 33'w)%5z͋]lnL)؄3}t?@ W a(HUF7.k˩un6C6a2?ϕy>?n&\ogF=ob %$i3-\nhinܒ C*L]!@V ؇?ߚuu6'zouk`k @UŽ=o緂\9P7d#@*mykR92H{˺[Of3ؒUm~w哭3g#Zv~iT 1| lBl Wh$+_`ǖַkXUH|Cl0˹DֵOsVqΉ=`h$oȞú:\O?WAah2?SY1nKxɧFc[dl?3=;]=7 ]!.g5x`Lyldb󳵹:_jMs'8ͼ~3 P xL[%i#{)d -QuvP(M["NmՀM>T>хVbj@Еz3;TJv. Q:lTdV2#׭26s~tVIS/Sn;2 =ugh0ɇv*GThBVo݈s5-_e77rkٔ3u_|ٵpmb*Sʵhq #U|jn[Ҵ^:K56ϻQihGYiug G5rxGimy*KIFj W[^jtwE.i擷(E]Uäms]; Irɂ>ޙuEv@#nSaB' I+.m\[z޴{i ]q/7e:sˇkU j-}較C[ta3m6ϵ$o.`k_eW4v8On7A5V߾Y<4`U~~.l˺Tͮij=60ʕCeKlY6?4დ[()*OnSQnm@OE&(.?q@\^ /Yc,+?^_ej+zeõ{B 6nK&(P 4NkkIx/,.6޲fU,/֪m@2ŀ$kۯ-dºί.Q%Z[+*wV\ ư;GճcP2jߺ{\kJ~vts6\ȹ թ7?&V4R5-^Y[2\}zv *_`|s˚?zyŁzw Z'tY-xj$:^N%y,S[$%T' /TӯHEI}e]'WSQFAThN]㋪_hoNFnͅ'j Z*ls`+[<_U]xVfr3;]mc*;Nkug?`-IϾ*8mo߈{d>=$ު2m Ta~u]_3Z^,&lL~}˧Wt  ʼnrF)=GՉٿ?dOR7݃ J=j^zݚ]-W͝vI浥NpY?y-'*;ST1Ǫ<1|۝ꑮ~:qv<0Fm*lȸ=Kk9]$` X%Q0)~dI#ZCN.䳗։v3R=\cw7Ou}ͿSˉF?0NJhm{m$%j^WoᒜMތ]fj3s/oĘ\Hƍ??^l$i+ // +M32(i22Qzk6jeK^ƚ;1͙]^WJӍoϑ6?R$24?c=dC4TYYgS#7\iLƙ濓on4]ROe$*mx%Iz87FDD?{wlGv߇{0 fp8$(K,K#%*$DJ+KS,Q%)CQ!)r8!0 `}=` uo>}VQZ41N5NspM vv3'c!]w ;bD5D1h&K E+ ?U3`?$t]Ao3W zhGu+Q^jˆtU<+ B[A+^$w[!26  8 BtFLHN(%Ut]_#8+u`c^qbZVxxوj]9qzTo!| )wK T  PZYʋ6 9+u =}Nw+%.Y3G~$qSr}x='-@y{+h#`4 d3`oKnh$z {o}WP z5B$ H0z.Ih荇5>gI #8 zLk\5^x؈i{ò h8V5dDHw%j9i NPuԀ[*p歶xj1wB2qz]y|̀+ruM}t õS9JorbUh0tQ.ZdZCDcZ=WAm p3X\GuC)ze߀vqo&<14#s܉kcS(68M_j&\ۍ jk.[,3\G1hvb{6sXx}^3C!}OwZ.i^5z k >Yp܇(ZOC\b _jm瑻Cv_Cxm4WrP&)Q]&w:fϖp%xF:E>/m;k8@ТyhJ^a0 Wm4k/p= kxl/h7\} 2+X9;!#5Ehv53Àaj9EwixIP0h#:[KWi(cvWt}@4zFQ(xJ Ab$w ](iZ. P⴫pIEx >>S޻ N#w1c8CFn2Ηa78}4PZ21M!ךvumeE *3pjᜮI鈤XF7{P5|ȱk. AQv(pk L!B 2ryؕ}O(]]goe-MB3 .|%of9H.}oXTܙzh31\pJkXVr"]2Q+mkCvm] S 4>$*+)*P\휶P=Ќ=hU,{ngK]~E1PtV =c{gjVrzq5Yx>v)ٸrS 3D 6 B כ)ķ=z;;T}t|Qln ؁m1`S`1B@I(-6`U}ow}ǵS0pmja3`Yyq2PXcr oq=bet w:14j])EFh71m8UHSM5 b~Fgb F2 q_mf9Hϻx'm@Innh"1 p}+ў8Nׁw%of9M_qk ݋(-[ ]HIajkMXyަ [l}eDmzh2z 4JMv|ù-\+zӜ֖o+N4QxLǥs{ |7@eΉQ,@i(Z) mg6vWBi*X}bAN͈ U P TW9N?StM=f:]4n}5D wl*G  >0zk t,0t$OX bp]9Γ:#{v "ݠdE,+6*vT+Gkk{ˈ$i,6 ] fE ?_Π): x3C ;?.?- >/ y7IwĦi m'^o̢dQ:4]`,NXb B@`wvZc #_wVj~x=` f €{Uy!˧΍^6J4lj_N7z8>[7JYx>nlagԓFhOb1^u%2EYNhģF~(>H ɰm;̢j1`78\FhHa$9TobB\ʕcp(O·onWۊSx^d5"i"1 MCp}Kf:>ix # m#8|^`D9`s xF1Ătq>|f1Vb2h <O C輝Ֆ(a_}鑞|na>ǀ{cHr6`~o~m;@&OoC_̰ pYu[q?u' n}I =?\Z ;[ ͤPl!t6QG~ Vc1\4i ;g{`8Ԓq_b >$6=ޏւ1!%]2b'@i(Y) md^10sьh:BN*d' G0=wOaްpx\w)v vy+{chcY|e1z u) ޓɽcn$p+O`ﳇnjPZCnÁ4|dn3Y/i6ֲC L{nCzqg4srnsdBMH5b;BB>v#> Ԧc N@ 4VosIW_<ƽw7Ğ h8{Af(X5*!n raz,!tm}`fPnr6Ь߶?n4#ax358#GDjO}qC܋wӿ|086k7Vr43Yx>n*/,BHD1t$'~yF)hjSLΆa;=ϟaQ#܅Py<`7Z ػb@)< }p4I<'+ǡŀxqy8B*αS3#gO \B+8M'&ړ;ÿy* t;<ϟC@p b}]U]qxt d$0r2C?ÙC(ziؕ&oQ##ۺ뭗 j>;lsN+ ;n:hx>x*1ք&D]Cab'O#3PRG1Q,N1NJ!Şi$ \ӂi>|#|(cK.XOE48c̶a,"{g5ADOadvlH q'ZCr%:Rw삙AǀC1 H`$">cC! ߛDc75z1 EJǑ~uY.)}J1`oښ͇;L2FNfyWBp5,X`#8Os€dPh )%|{4,7\^䰙4N|PmXe+<h z#'zgj n6K g9> 7|c)4g  u##3'{Kq54\/>?tsknn Q>x~fg#*`%rjgLLUcu$tD:=C9B.7oM rAB!p쒕,"(5XN F@z0D(gpk ʼnSא=؏O?-Qp'~B}hQj VBeSut OBp=Q.-w{}ÉS9<iIKy+͞JIF˕;V'8\B s֚n b~b|aBfzjw9gr'2 }xS={M `|(]OCD9nfۇi`1[ Tl+`=}`3oa= C` 'z$Pu2 W4mn5:ӈrRD߱!.8V ;DY%fYeD4 NA >H AG} pBr(?j|89n zid \qg:xb Ez؀9B.p*Ɓ9#xgQBDl;;iG38+Ob} ѽ},D#zG8=X0K&4DW"I +FTCI@AhPR4#sBz( ߖMp.MhV14%4M@x % `4 R`-x B|[Bj4^k MPR]FhvG*3:<[µD:.o=SR:  #GhzPQp8qta\.ΩsnLlx_;(&`'qWГ(`3aݫlMKHE4`DuD:WBh@$i p@AhBwM~w%tC 0`7<(Ēp4]It54]@)@I h[!Dk##!Z4M |Z~=F5xV)[_IZ+{vкpW gOއRJ@Db zTC=oQp⿙td:1Ked {io) (0Ѯر}I'1Ah\! o,~c!:ܩG4y( @ b=TꦦQoog7lK7cl {6 o( d"r @׾濷}kl}l>](u߻wǯ#|FOkۓݕw e G4WwEkm@ ߋbފ}om7D ~(<n7}u.>>kxq{{ {eѻ7 ,Ŏpmf Fb(;9C?Ǟb&ڑf#,ѭBiT³xSh.w^y<ޙbRã\Qv+6)Q ={:fsNb*K%D[ķx5 OYSX;S]@ _ga$;'kõ@NMA@rO%zd99ւ 6 AtI<v"m sBA`!5D0c zQksѭŒ 2Zk&z@J)xQ5rsp"ѝ@ ﰱ]fX=3ul+Zm/pM&;ָHlC~ !GFY -n3݁nMtV~ EVPP/A DZDkAě96y۷x8z{+ í鿛]g5͔T>/ס:kR -Lʯm *O$g!2@t ,9pNU_Oxyc&'B g-BS팙{MtCQQ IDATY51}|-¸Ѧ}q!3=k@0bkvل0p%3AD2POxy ׵*ːlȈR1aXPa!v_]FnΩ@Q3Q֍=L=( ǝb|2JM*͑ VCq90oą_Sso&B "1Da .RŠPkarSo:#'z4ݱ0th &羞|w(DZ5ޕ F_cfa?x+`CFt-~c+4J A9 e3pQ7)p/C*{MtWQc…2 ͊YL\+3`w%ԇz̀D\ WPk~^t0~KR)Q\\RɛMz_/BmS_Y0,`];Cn s;cdQ/.CVR![*lӖ^<>v O\Qma\kl כ4*9&bh̼R埮pF*/,:]U1\ߤx:ٟTf16QB@T|D;OQks\3d j]o#"~rf ݔ!فFh+kh9=u9ēh\aT+?&md ­,v*Ovy,\pIzhW/p޵s2\.D K?ҩq&{ @@AhsM~LcuBPg*>ϗ0nūݛ ?ܹH!Ah _sz>`W*9S› ϒ%TƗ΁M!PGDc|kYYzJsg˘x.4\2cDal %$3.;7]*b˰VJ\dBK@@sM.c-3\߃reX262XKhyv[io/ay USҏmggР%lũ ${W# {C".)&W<>H!i05Q۸2>w pMẃ&-^G)PmBgANho,OV81X3\w6\g16Niq^\2`5uULh.nl@]'KXTA8%kƟ?ZcPk*>ց}1nǀM *&} Qw¿<|;\\߹3SЕ^l|MD576 ڳ%/3X ULZm <ɽ$8䉨zy p>x XT̻e\na欉^ȗ0F>vWvmT׿γ3Q'{7Fa/YKff*?b* p,h c WТh2Nh3}Ŧ;e5,_aR p{y4U"92p/BIB&^Qħ~ > #3 Pۘ|5,nB< v%1%GWNO#P}N᝿CG5Q{#e̽`fn? ?Dž?9j!:g$v1{p"6m QQo+k;[Yan_~?{S;TXp ɷ,AIae3E7 ~\by1ytz:]C QOc# .`m<,]`tsF^zyES߸K_}ssP %#Sksҹs吐̲Ke\&_ ݤm()>@}g!{h4oD]?ZDqc_ "Q{ê{lMLU&C5uyߚ@e|F>~Lk umb#`sg$v]t/}fR&R`]Y11vEKp`p݅ML^Bul{>q#H@iColk0((p"/C ftdfŅkhL{5T\DtdpK':0pyv(0L' ICy)TBjbus* > p!P*+h.G7|c{OCDtvPc] rXm{g1p(´9î8&{LW`dpCx =ٴ#隗rD3&ϘXZ/Cicu==kN0̊ٷꐮjk``c:V09p/ 4heӶDD}D}1 =ƞiOHd",.)-4Q\lQppMxu0\C{AsL zbD[=DtD`{O&`iA?hhVʊ?.@@m@3\=14SICP8D[DȐMD7]z@uCchVXc K!;z; ѸΠE/|TW8ᚶ`~ic@,`'oуA95^(XnFeԩ |,{{"To FL\ᾛbP]Z>,w(zX|0\vs> } L!4ݗA/ }tol!DtW!;pHєcez(=!Bc-/;(/(h,PkpM;ʟQdCDo앤CTo}S0t>k`R1膀fh~\?P[k/W,Xm`=&8v`oL_oa?~,H%EZawk7~y% 8! '^?@yžG8>D:Fˆ#РTk0jߓp>*+ L,^÷$܆Sn`pMmje,|࡟?H?CB"qF<N| 9>dh*"$(wid=|k [uqЋH\þ{ C<A$Aӵ\R)/ r6. m a0\S*_E3 ?4Tz0E  h|]*[__RVc#֯8- @p, a+6T >C$#{ dH!4}\H3SW}H w]M]FсU  l8o PlO=7$%K(NDD;~ijۭ#s0Uq,|G"`tLLBh@Ah9P׆p\{J~=d+8 fŅYq(8  B=4gZY g!k5}6[?αD6zpδ:Xl`&. ](IJ"iy>Fz0+D:S>b)JfɃӠG5M 7dP_qam.!4_PpM=ēܵ %Xϳo% < @IњQfl?`, ~}s`vPpMOVXT";or8xowM%+\DDD @+dm"""k x_#m#g$""b#k;4{5$/Z)4 ,}Bl9 PYDDD JA |h\Ip&`Du"4K@DpM$4$А- 4P%""k N+$ ,ɖ >+l-Y""kN)"fO4̖-Pmonl""k =R:-9@$#e-B3.N]Wp({{!I4l C<7ɶb55@M! P㖆 `wh2@D"WrjMԧ6X"{=?}BAA_Q l@! ` HL "bГ m"ਁn0t =z0mp %A<Gj8?b1\{4I$A'oD >p -kp.s1\DwmJ_-VѢ:4۠@a™v i;VSDFOzWGDΥطT0b<=1\PWB9{5 b=7$~ bߖK(-…Z67E5?ԏ]{6ma5fa) Cs.TCCNԸLvaUDD D[TEPDžl)4 }|ǿ3~hv)31\ bC1m#_=c?Fou;+1\>\,ݵVz&m^Gǐ>25XPA皈h[[ᚶ=!ľ{$c,6 lNDD` ; H׃&l5faD0▼/žOAt[r'.L6첅d55ц@AM(0"L"q1g43KV3u0_~;=0Tlf"`55f \} @B@xO[ (]܏h:0p,g 6,Gd =Z1\݁[ 0<|qD#Lt+o.A: DD DwU@ѤDa,O1\}XMó=!7 P#:թ5ث&ADpMtos F8t 5ADpMtZs>߲Y "D2VU8MГBev ^5}Z}3fs51\.|"jb1\(4gW7,pP\ QBDpMf8ZJ@`(\c1\sɃLd5 ³1WY ""kꐶ Mq1)vc -W`hŠPj_d!N sy ](zSpMDpM^@~,)b˯^a1^mFu ,uc/$r_d1O&;BPWSRY ""kc-956RܺӼ$"b&fj9>\NGDpMroQAG ^e1vUQWQtMPDbB8DD D;6_UNG]E(M; /;KUs-XfOb{vFo?} qX]I,rt^k""k]~):7WnhWɊsqAf1/}̿SEhwEMŠ#*'Q>Z1\D)(g1c LtI!"b&j*JycXyo:kADpMNZa4Ń:V.xQHDpMԆW/7-..Cm|%IzO|6Ϛ1\'kę?zv٩M 3]\S(z1\ڥ ƾ̵*AmE)YȿA:ګ+.@rS|X$ADpMY4Z\ڃS5 "b&0YM!ܺ7&v "b&L/cV6n뚍S(ADpMnOX ڕ`zv~ "b&ϏcBŠ5ma/αDD DecXzs NbжR|As]քᚨ }k oí[,m[VEL}<ܚdQbQÂЖk$ʣKE&"b&r^}plڪ` ^ɢ1\S1 9 mO[8 2~|(\3?@q.NHzuSwoX"bD[9kaQhޏt*g1s""NÞk-֘1q(to cK DD D\18{*l;ko]:Zg1:maУ<_FќY ""k"?)qDQ>d+eXADpMDe@O Mdl=1IgvP:Ӌ^t/NfM'd $[%O/X uqȏ~wuX7Ms 5-o&%Odiuq\?Zq >;!(#Ju)NqqdUY5qf~d"VGe:JؙU-helRS9ςn^ /:G5p|X^*:m jIrePg4~ ׈jhu55}oUHr}Jvw)#hiT+[jʌ 7uJ%@\;IuBzҠ2zӡlLd?(R SK>7JM{53EHH 3ʏՕ˪P2kS5!@PsbUVֵpuVs*͗T]RnnRqdIY2iet(FjXYkTZ,*XZy͞tEnVhnʷWԶ?NiV\yJŒTT4::o:lV'OT__qkV\բ._ꪒɤ08뺏|d2(KD6Z# z~Ir V[[q ֈ:oiϞ}מ竧 1LЂqy;gMFGs=lH\'SSzs'l`\o`9=؍Nك `f z6*;V vww< 6G`r93q'w{`'د2@ UT1j IrZ(~}W~u]fns`ß丱{``kcvĉjooW\&q]yN%Yyn/c#]+JippPJU-h<*ϫ\^ן>JRt-cڔݛUo_p5⺮>VGW__|T*vu5:<ϗ븍/c>w Z+Cԟŏ522ެ}t1r9nqJEsssw Z#7OՉuȑ-?DP(O?Շ:w Z#7OO8\.PXj5yMNNO.ۿN5pc]8Accq5IN P>Ą._W!q4&|^.|קc;q&?:=~#qT(ti3]> @\o]3~[oixxHA(g}ݺu[k1ʹǺSCf۶ܽ4 q+o󳜖`u3 bָ} s@\?!c 11q0qX~B T&ӡA'Qk=݉uIq- Z(%7ѣ /L&u '?}*qq8cVWWE#>0)0Э[BpB @\?Ĕ?;}fH$E#?0p|/|祱$mХP(hF <{"o_hlf~WP x/u]5ݝ֒*f ko5@\5  q kk@\5@\5 ]HQcp]5q q kk5@\5 d$Yx|zIENDB`pyTooling-8.11.0/logo/icon_bare.png000066400000000000000000001137651513317154500171760ustar00rootroot00000000000000PNG  IHDR)y=kzTXtRaw profile type exifx՚Y$7Dq98X "s9~=?DJ<} -=kC_÷&Y:_:Pn|grOo/()vY:}=B~ S=^\QuQ. Lm ?o7?\r~ߐ 3`qsHc;\#׌I~l:x:~_Kx:'^ ?s>ÁC{XWX:*U&:X^;8b[9xiȇv@8wa1!_C*bl!PN&+)EBq%VsJskcQRM0(4++ցt%\J^F5\KUlVZm6^z>q$hQG}1''yr'Oʫkλ{yI pvgZ0dيUkmؼ`o{׾ǮڹZZ|ڏtkD'E=c1:3CQSɥT",j b{~t_Qݿ۷9S~ܟ6i ) ֌l>}t]nUfo2Z|̲x.d}͹Z*͕Nw8F6-Xmv}E=e_.gRek\lnEc2ˉ;v@26b%gQX?'৲NK4;1e5W1 t {2oeQϣqI#WPCe5Vg FP!ۊ_;Tnc<' jomΥv:i L bJ[녶֢"g 5ZF_H -O}Pܷnj.ʀ XVK#TCqiayťkqT1J7rc(}Npo Ï0)2d96o@|DC񘭯g to5]pv3?ʹ@ۻ H3}r 6+/E'D 벖[D$|Ψ4q` @@bfh- {<gn2{tfפS(*Ob[&~cs_+jnkuɟh+dCǐ6Bgd gATo,l>`4xfmlP)\S_V˭L#:LGE6EG!Y-_G_r$J w ~Y޼M@A3]x"v6wʞ@W a_wn}z&TjD}[L4Mބb 0IUP r׬k7bᡶ:N׃5t+ W z .=~p$ckT=:nڠ(㪐uWXOB p!-@-b.+Wb."@ \ە{3< ZBxلguUvqukB\,)hV܍+w틶Edx-`IcJ)?5 C,gvx7ҭٺָ.Bl>ٷqM_}lst`92-Qc2QdKK<#Q)#Q<%/^N?5Vh'$sџ"M0Wa{WgSwÙ `6LG$!t4K^#"g MiuBRj̎&o38\pH|RKbUwZ^i:XS | Mbp|TQrXcTqqz w6IpRy;?}.|W{͢GADmhr 4gR%}]1hhi9rXpѷ|gZ9OLR"`4 g67r %#0uV2-'n)MVT^챎{Q!B `>>"z7P>*wUP* 菑*t>ٺS 6+{w9i.,TUyz#{q9,\+f a}[2Ȕ nYΉLF˱ 8$Zw[+j[cPf m2] #xf;e#sivV$ E&6ѸʖSmBPGT +-MF\s/2¢ٮD`Rj-P7q!VL4j+^Kkҵ?9+-*c?f+A=%i;oY^nv!zXI,Yؠw1f2q;0X:E 0 zn{"-qKu bC\@*f36d.wLF@|SO;b5GdtqĥSV{R3} yGYY|чY0\h8)iMg{4 "k3al/?gp)k +BPGAN 㵈9p5ivrcLw .:?< bKo0:"D!αc`N:؟&}]ED1I|'݇Sh\C6c (6 ͛pe04y}u%\63͉0E lD\_ T%7>Ls-5Hݦ`{%D'0?Uѭ7Xm14SIg֋S {O6J|Kw.i2|I h Vtqʐ A~aNr @e4鬤Xe }4ܔZ,qLB} <$ nB>F|? 8* ODb. i=N*5_1L=}Z^[5-PC?=c e qD9Է,Wp !w/`5N;<=~c}M^L)`(Q{#)3DQDS!CLNmI}2 EJj\JV1!#{w}N{ Wc> n&xs к *3=v3ӱ%v'o Yk̠YwyDnG=7Nd*{JCH?h']5"HQq3W9XDĜ'{.]&$plBړV; ~4- ,_T8ֆ  A[>UxjLn$=߮/o'oQ RJ2 v]pE{leYDž !\alȚ o;C*\Qzh\IlmQd2vʘ](6pJUkf[ ڔrm,T{IFB !rN*iߕk gM2Β&9Yf8pG?y1 M'h=LLn>HM CFdTnaFQ;̀CF`Hf4x݅: K婤7`k͘xqߦppͬ/[KX?%TBJudCJ#^,5|΂B]hs  =`0\t^}Z@Yqj3}2u%~} *+rȰCKVym#ɗ\0#381AFLA|Hf71Fz1 G ڠ_ºnM2Ww~`FX+ *KI\;z>M4Lvs>hV `tCp ώgw".KAqKu푑I02|/ 4tfeL t=c4)>t Plzt_^C  ՈIΣ>ٝ˱ ]mOO&m`MRk_aKwogZ֮6dA2Ɔbv^j*CoQp#$Ltsܦ$ 9L{8`(pnĘU+BGFS˘ umP1dA|{%!YkNʨ!ЧZj8}$0+8hsiCCPICC profilex}=HPOS"3tP,8J`Zu0y41$).kŪ "%ޗZx>λ}Ш246Ӊ˯HW0 "efBuOTw1gxMN,A8V/m.Aҟ\_YaN9 )V <afFZa@=98p3P(# ruګndUm%2}J DD1u©:Ӈ+ԧs!֤dr!p^m.|'*ݶ}]Vd7ӊNwe˾*컯ԡ:"Q[M%_.⽡2Ә/JKVDAVDDޑ ?`rh01T:0c6:Pן -\GBdW=*-yc=|n^eǥ8ktWB+[hEVD佮0@}eOqUQc c/uεItD)׊!Ss rHhRQ4N}2"_X*( &O:ʫ24DåH}:ıOR+ĎVu~'Vx򟿊 k.Lс"" '"cR|S5̈ 1Q3L7jYOG<9 QjV0xИ|z/QɎɻ9t kz{*ptKjY;o Nn6nXb_G_QC_9qs*aSȻ?Hxg" ""U8='BܦC_Y§GDAVDd{cN~~3,P(zQf(5\G!;!VM "z =[ )caT r̊ [Ps8)ʎ"?CDAVDdE:яL(ʎ #=_XaN! +"|m$_4 bYYrrST"Xb=OVc(Ȋ?~bfl.-d *yN~j~AaVDAVD=0+U.pӳ|" ""灟ܑ art>=˽VaVDAVD6|?ǞS5Hm!wL>=]_T{(ȊhQ ٹY`GjjY#"ʫ@2e ͕8rQa,?3G>1j O8fVPC! +"FŅ{HJQ8)jYN~aƤ3&! T3D5(ȊCylJlIl?6'(hQ99Ƨ *̇YoN|{`ݴ" "2Wΰ`{BwPؾM+y>6QQcU>!d^MF̝VU/+ +"#eQQ] b%̧&:Sc(Ȋ([jj"KG9:KDAVDF/qbe(>Z'Gj YN}}٣.زxV" " yPmxb|ѲDDAVDve_>' 4Vv(ŻXC! +"ѩόx1CvD0{zKDAVDv`}ndgr_{Fr(Ȋ:{Kک@vJ#`U&NCDAVD?>\c@ :VvcuN~F ! +"ĆS2Z0HT, +"Cm <ƀjceT4|?۫Q >2Mu2Ѿ2Bru5 Rd#Q4uBۖH/yɨ4"N|\$(Ȋ)E쿧F1d$aj1OdU! +"hXP2Šc9>zKDAVDe~lB5ƐF=BDAVDž*V;q>/!DdE$̜#j  .zpR ! +"Yg€0@xƦ@(ȊHL}*"5pE0 +"67O!D^'<{" "UG>?:V9Vu`9TVC(ȊHV͝3 TV C0QEödJ->Ֆ["o9XGc'WW" "&04W&dE9p)Fdha`|%h-.H ! +"BsHCDqg(ȊH&D "d_؁CDAVDpSLc Dޙ'MCT-! +"m`T+C}>Q;*W9^PVW$򮢬kYDAVD8E]=5<3['}hge3OrS4*j wP(4|VdENb@i@׾"5p[G`B(Ȋȝ 9Ki,GS7$r[h;YY3reC}*j0K|b"+ +"w/A!IMcpSւ]c.)dZ\DXLXPtgP/:gXN$ p8oX' ՙ"Qnã;SScjQP˓ScV唤xw_e  9p`C#Ce&S/V{*|5U]#[rD4yw3ǰzid6ݦu#٥[\G8k8P5kؔ PY \P0!N(4իM~ou٫mp[=]eZ* G >ьPFYPȠ5Nq ~}!~{A8u0MJӮ +"20B ROnjvnO0X#O+Ȋ@%ʇ*-O!:vvNղoاcEAVDdp\דtx8.ue#9CY%QB#:X4Oo,RkӏY1b(Ȋ ,lyNKH}J`,t;_Pkr(Ȋ Jho&5q)٩-ETU[ p^/MߎJ^7x5❡0g>]q<BdED&W(cdԴV?pT +"2(Q> Wס} 7di; 73qN* ""ӼeZ[[C7K .;I6_(T"]>YֺEAVDdPnToBl6Lu1YAɕCF>$ڛk)[k J[;/)8MDAVDdPқ'{hdk6XDdEdW4T5`R=\^;>E u-.Uk`:0[Ǩ0k3Uh[T! ""-r/0&S{n&lvumcuXNdED4FAa6KeQ畴ru̳ Vg+~L|jd󕀸3-jdEAVDdï80[;[nGץ_o6DAVDdpՐX- SAl&5l6NsS(Ȋ Fvt]hǘ^ ˊ002 ޙ?*Yio8ֻGAxO3WlR1-[#*)P-UrbB sp|GIi/m|yrjtar>Vc$ڤ7WDV?bsY, KTx" aS/Q+ n};9V'=xwAg ^m)>u$+Mܖv%64dm gnۑ:(Ӗ5Ҷ'[5Scq5RKy&u90JJ%iy뎸nq $ZdQlX cJ35"A!Z1#b C07@:C/_umXn$i;뒔F 6/ҺE6Zp../zdc[}d.ώO {/E+ A-Ad oP?0AZF DA}Sysݑ^_ r{`{MgCwEwCMI WXy*ͫNJrC`ktz z/[QdBG,[z[o!hZV]F@a<-cb%r1w p{uzЂjc{Ba{]K}oecΓ&5xZil,5ٺib |ldQi^OI~{VUJ ,"cfj0S,Q`}-w[^c C'WοwPIkR/\g .Uqtۺ~}+X umѹ:g2]#kދQ)<$e@x}e%_ R|znc As=Cc6Zk]$mճl\ozM{=;ؼ[YyK6L7Oa  E*{Wۄa(h!*hl]7a4WI]:mV^gçzaW҇Gʛ*NiA{ˑt!pLZG`]jdsd/h T'9W"rŐb,a1[t`|ܛ/@qq֮6I<ͫ_&smԫM;^VAvDT#0Y&?^b^rc[l|Dm}=.u8t6Zp CqGٟpl8vjй#5 y TxX\) ,Q> ^$ZuZC`nfw@V.`ʫ[/xkK8lSA-|NqC _zcQ)M& +]ˤݔ˴/lj6ҮvƖ{XT26S Wմca {zo c5f|}wc,\zf捄+]Z!AAv|XbPV20] 89ĉ9\r\y6_Z%*mt[)w;?[og3Z[Wo5>8u)0My|gpweKfwZ`(Vu B?$.i*Iagg[FKX'>5b||9 ·残z1d0X>WetKMn\lr*qY ;[/ء)BtG{ ӌf㋫~^s]OQݟ`]^q`BlBlCRgvF6j{~r/zaݺ3B(ctN\> kWZ5V僧d| \Z ~a2WO˾;Ε]5ΝeH6!Kdb0VC[{M(8Qfǎ0s^n?Ƶ4,QQ&[}MkmCPХ p+gCʳy1{NW9 6S/ F64X8 1 J'6& 4},qj\L^ m!W&q/(hۮA8vs{L4=# ۇ ^=ww9sDۥJi(T#JeV^mԯ_gNCTGIDC_=Ce}:(4U%_+4ןȋj>JSӄOÚozrwzFI۫]x5+4&U2uόZl0}J}6+tRU Nv#tƞO~`lBx yBj{7JMpOq7_Rџ'M6,ӵ6o}уBo`t0I殭1 QŐtO5" _>R3N뫘 Wɓ;6C~رiۧzWtxy?+d9oqxӾkK)k)  bmB$߾oʍ,~ľ2و6U>l`)T"拌/eZKR3EO8).9VL:>tێN3P;~D9~LZb02r3e3P.PnZ0o\g449(95(q}(׹z.9^XWüN'2D9eM/dh&0~NgwCUrECm*=D+G|o'K,GC%b 3(4JNkUf&ʔgk<=FUg*0PQ,ZM\M5T#Mßq#9=F4 Ո s%6<9ygcS-4NNsswWjF!S|4_ߖ֦ck͑+mz+tY 9_J8gp)LWer1f}ul+Q\R#sy#3$+_%]Ӌ,V -mԟ^}l7 a%M-g~zySdXk !Ahy\iPиo?kŠ~ vw@EⰽB2 9}<`cPb5b S) "Kr> "l~b?8Ja"i '(_C܆VftGkz٫/zu鼒ēS<^WVV!W ?^g`X̯%)lp#(L(mBc3.|0>u|wȁB\ȞOEJi۝@da6,DLc#ǩ6Iڎ^V]'SF*8o{d k+ ?q;FQJܑ01IJk#iבldKc+20c] PQU}?&?;EQTM4 Q|.{K#jHy"=ѧ밙:vsͳ>Z+8, ؃["a>b,vpl~bҮq+#v LN! 8{)zd&BX893r'm.ܸWЍ#) h.O>&lxMޑ" V8GvVb%ahb3'G/T,zR̓ ijI]v׍lO.0dCd7]d'].z *af?`d \A+T#|jdo}cȥDaVAG*/3G<>;P%y=L|xF \׮} MoAh"0 nLxΓs)&'mOQmA:=/V#Efdў#uЊJ1wQ/ZW8oq$6IWYٷqڝ@^\rsd󖸨b mOI¬[TΌ1}"ZQw ~7yOēl:XJZd-a ;[avE>|CS*]F`=Tw号u#acܗaPn "Ma yi( zuO0}^l)ew C_:CݠX )G}ť}dMH"10Œ{S'0*yU+UiJGjt,iD 6}1ZfeEvB9fbo{|$+)Nu7ɮ6qlO"h̭Մ.n d'4抖\1@_"5܉;Æ2Ij{0*)_{0]3% |?҈쪱B_Scea>7U2r,P_?O3+t9ڂ++V^m|#r''xfPO#5w79AV}M%254MSwŏD(gt|L}uE3Iva "KqǦ>TAv+G5n.DFd6DchvBRiE?sk?>;9c*y9Cӻy?yI: lL6ɚm֯5umDd2aзmm`N9I7A,||ZA`)594c58}d7uZi~7.vކɇ{`O)XbKj^ޢLfՄnMWFn]kɏj 8ljd;ڻuj9˜t J!s8Hyjɕsܽ]cjyapܑ:w [:Cd$ 5',f k"Ci>׷Dx?yFm!oD >c=A,1{L,Ws3wjyݭ3w؄(Ȋ̄C&2c!(4KiFiOU!Lȶ<Ab-$[Tg.2}f^QK>dO Ϋ1l}dηܦ.Ȉp h k = rjcE~b-Skj f`zk[@"#Niuka~p<2ޒ!I7W~f1S}]+Óg'Z-^l^Ihl~&l^ *ĽK~ȷw!#\7eG1jW˙8RJ+ g뮥\2P2P.ݎcM\@ ZBPl6B181v;ܦPDہ0|@|9$MCT>s~<s?c;`{7%Bgtk*VzU v\hGPl89K^f,cԋ kf[Y67 i]+LS3߸³>4l]m[}n"m&}t=>< ޭat %RN[x>:o{V!5>_ lC3&.+Ⱦa&I/? o#Yмx :S**4R}/!xl6mB9:-YX!R? '= rBn ˿]E.}M"cǦ),V32\ݨ֬(̾<>t%+[|xkQqQ!Ux~J .5m¿>{Y: ep=_tQ}Br\xfk*~ş+l^M" 8AtpE /'f_\7Ϫ! Vcov݄klՊ Rϵ߻@g]+=x2_z1v]n0+:^-8֮bޓPwC,`*W=Pwڅo)c J\K~O,+ &ݎ+LzvΦj dl5J V^sq;qy}C 8Ef^^ZڥM\k']Ny%^}j՗TV gU#\n|>Y9{\HqL<{;sffC[s kYM& X֮TS q5 lgMsiSw`=<+|hRoиkzw\+K[eV Co.s Du\>֍UC 9#U^\]og\}ՑC^4]}ح}f:lH>ɋƊ=w:tCA&^ ,#aK3Ȳ39ƾ' fAvaԊ(YKR%LK*/TP*TY*Q$^1/\,2i09tOtN7{3 jfss~9y&~OEq'ћwBn]X ]fRj!$TA39BM 'Nu+K~tܫ+\d}=d >>avr}HaVכܹVgzx"HA2GpⱾ-vzg.C{nٍ"ٙͲBGZT^Yw7d}![Y,X~sW6S.Y'N_R].i]dVjrxf5}v Cjg[[4kC# ,ŕ3hV{m{jSqbW}.im\C\E3!œqd7癭bL 1ꡢ5\O30`&hV|ՀچGPEGvžBl+BKyCI's 8ag.MBbvwBlqMv١#5x&AIiINRT`kgY;@ɡ4鿆X!ȐF lteE_=uJ_jL9&G$ L,+ߝ%j4 Wi.N <8ڗl|3C0"V(gC@dCZxU=4-h[i<†Vq[yj=KHv{f&3ۡ_,3yC.SaV?Cld[k_ .NҮ Z3;OeN+ ~pg'\wf5fKh, KDmS/f)\SomlSmL sI! &֙?~Qa6Wϐij}\m&+g- e[.o-yRmd_:gS-Zګ꡶M_ؐz*V_v3CPX|qr]{V\(0oo>jo3[X dJ!Sey3Fqk-͊"kWx%M%dM !C.T\dԬ av6L>(v•&Nnt\}$ -zTMDv/Ɔ{Ƃ铽BPWuL|;̆lنl'ڸʋY(bYVD~J A61Vュ=j^8>l9e.AZ֮UXPfeM%d{=0[ӭn8!rJ|>601%7]c*֘ Z@i˪A696AO W8և5A{<ظ^F* "VH~Nzx8:ytB5ם6jÚxes =FFgR5~N)tQʴ3 )vgd[ׂzګyNi*K Ou7SSX0hB6l;iT5SLXKZ/RAAjE:lm C![W0?4f{BX 5.VC b]StB-c,ao*v+.RZx^sg+*tVm(Ɋtk5@a|_VԭnH8Au/WnjY3R,v/*8G+v|3 EslU [hj\tGwK]}]a6 :a`4UvVJg qTFw?s33.Ђ0; /,%MԽmC*tNK?vip?kQ]l|yVw6tD'z-Ssf;&N=S߽N~tqk80$U ¾x+gk궆5c|bO߀nٍ~W0{;%N޼lO20'C,PIUv -a+Y= >0{c9R}?P0 p;E^B_u]cH38WV:$=CĿR `&qo>7""w,̮׆)5f; %|w n2cIqb%di73DP֜zn2Fb?4oT f;J~*5fmIuCAo}3q{?I@Z{Xƒ}yO=[Ն!aVcfw]زL=S2g*H7O Ïp!d*Q4cxx?Oø%ґ(/=ܭjʜN;fo 3(և5AX?*/ΌvK;m 5cA.b(Ñ=Ibze;2ldb ;~Aorqb`8AH60A^YzǐzvH +9l9>dH`ϓ7v+FBl r1Uً-]QU1'}OhR?{ۓ%y`@{[~pC.i'V0Z|#C:0A^Rb `\ Itq?'je33mWUu+uWaa:0Af'"W^XS1:Xf)q&~6{o=ž'Ki #j҉c¬u35l𛡊1bhOpJlAO'޺cKʼngM1n6zQi5 uև(Fm0d7!rfb /w|6y >}koVXfwSa"t{ _$6q mQ_E77ru'87BXPE0n*4T.`aH - fAӇq}AlhfwP,q5  *7nib IF0]A3YZV!wc?#P'?Ç0,ź" ұ-Njcޛ#CI` F+,ְIr8Ertze!d_aV\so Eٶ[= \lu1?9#IDA= 5:_<^m9C)70{cl>(lYc%00`Ab*l j* u:4Ɓ#q@ַ >wPJB;ۆ[f3g"d`O.ͱ{:ž=!3g\sd0gUBkfwRyp<2_^6 hM}5O2f0Pmj[8Uau56V& ;";۠Є']&tLqUmů]0%m[tB|ݥ q,p 3r|R;ma6 .`g8xp t>1nRPi^ ~Z2_yc "sŏH=Pa|f/VI>2OddwPJµuZ5'1/=ava &=Ө"ۨa˲6UQ!ء$29mw&N| r=w'㣾;wǝ|5vtۉ!L>fp#_xk- _wSsq`cb?>$"w3k<]ʍ;HqFXtIx!=# ,X8 ḆX!#ldiVK:T=ldܓ hF͐P 5xi1q߈%>`#!w767#Bߒan!t͹J`#{s г\fI ЪC0c["q 7sQ`q7YrכZ;tL"hV-96q5]ƻ/6Hjl#_xeOMM/=')SU[ko7),;8bt!1|dD&K$.a(qc bC,[AsnhE1C"YA0Bw!v\C=`-b3`]%]\#p]FqF@>F~#$6߿,a7͐t[!Q`qbg1f3~[6߇=(c'7vD17PhPͶ!,QhmV1`2CG'O?(̦3Ild=8O#8Z!o8^/ ѥN7AcbCkok6[%3w v3_;;Knyy[k}zwڣ~_=+Y9k?*׊ wf ~wo 2th8|,WS'fC&q=A_׽{wܶyy{|>t'?@8\zf;*&Y#gXGއف=Cs!XٗܯS!D/L&b~3\{Dm=Ih<ڏYzu=T"GGȽ,ͪVl&Fpx&!;-O3738 P\s3GZ/ hh|Ƚ~5MDa9/ovl k!ٷY}ZM-%}ZŖj! |{E니|hݼtr!&ko,31Sq Cd,V "c;::rgL<6TxG!J" Y^ZW!D>@آUyd߷(*a=C,~3+{61O^YҬ,_&w}査 C!"'a+P!D¹ rR˷j!akwE|˵ߛ"waW+M}BCyW (֙98ߞN7< n%}YPvK3/B6'4j>KoN7lh*sD(7$G4FVd'\v_!TS1/Cltȵxvte]}aRdžl!"z\B_`<;G!ֲ9"`z[f* ƿ|h|xKA'iNlj^KAK^BKqNU _ts/d/^Y;ʳ"k> g zCF =f_.J Xl5k.`n0?vn2F,W-DvP}-?^!;VH%bfӤ\<5U:ͨj ֚\1Gx ! S/,Hoz+/>U{U9P߽ƹEuIVle oC"0BUVx!&W_\gt3]}?=djh̺0ۘv}…h/)aQ{\;e+o6v=9Rܩu ե4[X \M%0{@NeW%x2 gb{*?/Qb "Y"u U.=F)!~D5bb֜^Xx2^O6jH Z6XZXUT:j>Ǟ 6,}ou,`GcdEv W*tg,^3`L .uVrza2+%48 K^/K2^"Xx|+yW昕>ΰ؛{4tܛ)u.ڂ\:H?6Fe7s:Bk9Qckk̝.1+y- }`pȊW~gY{r~bs~¬ep<"-?^bJHB=Y~Ƀi.}fs/T]%"<4B<(~\}9W>iEAVc\f7ĵײxuD!VA?gk[+ы{q5H'{kh6QUs^7"^0+}RzKx<9%OaVbd\boS[οsXl)D: "aTbdwmvX)Jڜ~Kud_Wy-K+̊B]Ⱦal>#[Z 1}jSXٻPs1P{  "h,/0Z _gSMzy_*08#D8ZLa\ E U^g<ä*ܵ0!b%.1R|>s86d'"]f `4H|oeȠzgZcu2K+d/z3*~|s1p`QE -bSϔf8‘G%չ"oT6LE WIDATe\S5"NQ3}'wyjjz ZuWZf@eɕ֙=U`jOXٻyڋ./j ] UN4s!oWfk[!\|KϮ1f7=dBP9qOOSNU k1J"=c:鯽+*`#Kr9^f^.V?%/y^[_yOI1"=[hOO~ -ח!^YPdTSZDPF_=8qzyHIA:8&ғNu3EEOv(D^X:_ҟ3lF7!#{ObHU* y=}{q4Q:O":~i{?OyIayO**nM}k?s:Jj|hs[mF(qU('>? {!Rg,&Kx՟]ڻQuyJWVc dp͖FU++\z&I~zNݬ^!l6{Lu^zƩ, Ee8dO7nHZH}Nnj_2ylGUw Y1WNUXZ&7נ8ߢ@ ;ㅖS=da v#[E 2j*&&F =sP~8aR)MȀ㪷M>f._[;mUP\{́8 Ə 08$3čk[YFfgBZcr7I! %`QD!rI 7m7qp˼cȀ"15݆/V)̷Z>4OHxhXō;n)~DP\i^%7WglU#ZbdXxfgoydZ 8C<#c4ΐ{# "h(`!bq ;lwͧ*s/WpS~fxc# Ɍ$I lj'Ĭ od竔כ\xf5l\m#L -` +w"V[Os'3~RXnAfs18[vnʳaydG; A(ˋ޽>tr_G||+ A1iP\<&_/8pS N$<6xD&Fj(tZk71B?" 7 !(QVBL+3S{w!}0Mj"Ez"Mj4Mr4Mr(E|8I=dd%3 MNt05 -Q1 ^3 "?bbV5`r(77+ ÂrNZ}wV̐Hq=#3i"^cb~ a$Dapi&dR2v0a89?,[|pXa&/kѨDaDP\nS\lO9d4 jOdEn Ոyt-͗ X6j_F ڙf(M* z LOqaVDf,q jd`PP鿌 8[57|x+bV5^=N!rhCldlxUL" "˯Mp5XD:FKUިa\CYВvT} qp%BҬ$3.B=K: Cң_SZiʊGbCp146޹/ +rR#qR%wxʎ߹}G4N_n9[@bb& Amknjb4op*w`ȝ-%yPQE:>a YoBkwҽF^-Ոbw*Ȫ"" +CWٍ(Мƈ -YL[+v[?Y/Q!niet "" ";0UeUb|/XR)DDDAVdGX8Zjc%\o%@DdE+8'DXdGG{_](Ȋsgt-t@ *ɶ 0PE&F(Ȋwgt,xZa[Q']_momlQia)M҉ Wsnہn t'TIEOhZ頽KRc"5ؘC, GdGGVDdEEoc _lVHoʼnB⑇1 =Է5*3kV* - `b#GAv/܄Cj1DU݉چ!Q@Y,4&ԝ>]#^`#mN7DlY1\wCvivlQi(jZ;]kkpyW:?}ZrxGC,8Bc#+" "+AVvt|pa&&գflFSY] UV⌌Qi7*YCldCؗx2ř"l qR 2xQiHkbD|_y-?Q []P^.2<4*/Z"k0Ne>|EGܘkqܸC,#56n 7Z6g,0QZ ADdEvW +a_Ck $1 9>Cs˯;qֆ6R鷤HPk݊IJ16k-" +jDy"w<4/+ȊنzdE߸AJYY QGkՊ|8׉1:VDDAV3Tf+f6|ʊ| Ĝ[" +1'"/҃D"! -XQ^1dy_L"\b%¬(ȊtiCRn1VD>@= li(Ȋtʕo7} 1R1RzY,|jY'b\BDdE:NJ'p4@O#R(Ȋts8s+4*(Ȋtb7꞊!ZKk5KcbH 4=%% tmj]YNVU8hkB6.m6tDHG ! FS1x% dEDAV㭝[UU{Q`)^_ïxQ^q 9oQ`CK*Aqҟ`T.kY*׿sZEҗ 6gT Y6%ۨs"J5b&W!" +((W֪_VǍ 74+" +]iEbNbH_i[xZQn{Hunx^ }5R+Ȋ(JWWЗ*tL|*~#m"*3\T1DDd,p3 *4 4U oϨ"" +K-e\w4Y=6Y)딯һ tZ/RYД["" S_ {nX* YYQ^(4plXu" 9*Wst(J)ϗY ?ef u&" +uw %i yDzF=[Y}9XYE7zsVKEomⳗX9pzcEDdCmR:wMXZ[/lA!VDDAVq.֚*tY S1DDdo=COd)pfIQ~TQ| *t c,Q!̏f)M7TYG6 E7WT Y k"csK0&ʹ<.DDdO!7PtU/<77TYhy>K3[ҡ u"VTYwoP_+QV̷f9R " +>~BH'56~}oM"" "l{h$ R_!U YQ14e 1U,0+yCDDAV-?3n̊V-7Y9u߽b(ȊܹoL!bWߜoU1DDdE׮lUŐkMqN"" "?KWhk*( s \Nx=+,pP1dB} gʑ"" +r~.6[*l{56py?=G}Y]"" "([;󗉂2+bc)_Ybᙋ*ke7KqЭ_نFXSoUYX7j[Gҥ},/gUS Dvկ]qF? (+εo_$F o;<<}fd߸7.Q7DDdEv_( [5N8 ^̷P6DDdEvQ؈i5j=tPr`_djNbvY4aᛳDEt\K̋arOp#1:׸O""Foܩ[kRj#pSq`Abk-2ӿ}V@'1e_Yyb8F]=( Rk9߽t( -G} >?<ȑ &WQLgָkn-Y!7ɡ<{ 73qU c)}*%" +҇O $C'a<۩usLHs-%jڈ(Ȋ$p?}8YَɰXki,Yz2+YsuCY >z:B, WcV-1g+DAYy?wc>(A`Shk5jqaqᣓ' z ;`-!^B3W4c3 Š(Ȋȝsb0p4c!m7:BܥeWW֨.6<JDDAVDE`psG g>:Yqc,j*~@*˯,јH"" "F$?N,dhQUZ Ē,2K"2ASuU1@dw.U)]88!cɱܘKdeuc ÈFNT)QdXkkŭmUjpSɌ!I&qqp7cAHlV6򬾜N$" +"mGkky&S#$2 %s` 7bb^tcp:RbB*aDDȊt=däLdHOdqp\g>,VxmQYUhYuQZ0Ξ cI2iqt00D\ZAPCBu˴ jDDDAV'ajľqHH %c81AbYn\"r`fymPQgaƎOf2xu d 7Æ9#Xmֿ3_` 8D~@D~HF`#ET`#MY c'SsQb(~,M8O`DN hwp `A,QS) N:M}%y6.d)NTtQrO)VοDDDvtk`!(""@DDDDdEDDDDdEDDDDdEDDDDAVDDDDDAVDDDDDAVDDDDdEDDDDdEDDDDdEDDDDdEDDDDAVDDDDDAVDDDDDAVDDDDdEDDDDdEDDDDdEDDDDAVDDDDDAVDDDDDAVDDDDDAVDDDDdEDDDDdEDDDDdEDDDDAVDDDDDAVDDDDDAVDDDDdEDDDDdEDDDDdEDDDDdEDDDDAVDDDDDAVDDDDDAVDDDDdEDDDDdEDDDDdEDDDDAVDDDDDAVDDDDDAVDDDDDAVDDDDdEDDDDdEDDDDdEDDDDAVDDDv@t ,,," " "  ,,,," " "  ,,," " "  ,,," " " "  ,,,," " "  ԆJVO`IENDB`pyTooling-8.11.0/logo/pytooling_icon.xcf000066400000000000000000013043141513317154500202760ustar00rootroot00000000000000gimp xcf v011BBgimp-image-grid(style solid) (fgcolor (color-rgba 0.000000 0.000000 0.000000 1.000000)) (bgcolor (color-rgba 1.000000 1.000000 1.000000 1.000000)) (xspacing 10.000000) (yspacing 10.000000) (spacing-unit inches) (xoffset 0.000000) (yoffset 0.000000) (offset-unit inches) gimp-image-metadatac 8 8 8 727 727 3 567/20 567/20 N3i  .,yPasted Layer #8!? "     %$#Vu " 2 B R b r   3 O 4hhgD$% .4^7G7W7g82@DK>TLT\_a2inqqy~S |TB%L1Z#&)(\:(o/01;A3ACLLSZ]4fooo!qe{fT%5EUeu%5EUe . / 0 123456|7 . / 0 123456|7 . / 0 123456|7    |쓚8z5}y|2hW~/ːr,󏗀z|*s}'݃zZ %ݍ}  |852׿.,* ' %  |yi8oGfdd5ujmgaYkfe2lw\_ia\]^^/wmhg])[^``][,[fbXlk\^[\]\[&spi`c[aa\\['olgYc^_[\] [%bZ`Mkdb_[] [}ny8 Bp6Lk2$'Nl00F-P + ,] Ύzz{}`}z|xolÉ~z{[Μ m{|}}?)򌊄3􊉊|979=>?  Ҷ)3г7;>>? ΁ik]jifa][|gcikg`ek^_feaaghbalgdhru_^dihcl`0ɗ bfacb][sjga]]^[[\\[\[\][\]\[\[\[\_]^ejj]\`bfiWot]Sbdb][\]]\[\]\[\acg\ceegh][])[][]cfhQ_dhgh3[^_^[^fj_``7[\_`^^c]`8[\]b=[\^>[\?[ 'J%vlԤzu–,O7 F{ʎP7T׺| $쭢u3/p];5ˆX :٠NA< >|t:o8ZV531 >:Ӏ8531 >jm:rX]g|8b^]Xc}u5]\\`]gU`^_3[\_d`^^ca1>K2;кZ1 9ٹe- 6 0 . - ,+3*? )E 'E &B % 0 . - ,+3*? )E 'E &B % 0 . - ,+3*? )E 'E &B %==<m:n9o8p7t6w5y4|3~2}1~0~ / . 0 - , + *('&%{$u#q"n!n nnnqu{  ! " #$%&'()*+ , - . ~/ {0y1x1v3v4v5w6x7==<m:n9o8p7t6w5y4|3~2}1~0~ / . 0 - , + *('&%{$u#q"n!n nnnqu{  ! " #$%&'()*+ , - . ~/ {0y1x1v3v4v5w6x7==<m:n9o8p7t6w5y4|3~2}1~0~ / . 0 - , + *('&%{$u#q"n!n nnnqu{  ! " #$%&'()*+ , - . ~/ {0y1x1v3v4v5w6x7w8s9p:o;n=<=ۅ<;둄;:߃:9ܫs88{87ꅍ7f6i6~5T5v55e442}v2||t2x111~1_w>=<=<;;::988877665555442221111ɚ>=<=a$"ԧ  !""$%'( ) * + , - . /01234567899:;<<=~$fe`a]bb`]]Z["sch\\^^\[ cdTtb_\[Yyej`_][fb`bbc]Z[iET[`_[ފ_djg_[zcYbaba[nbkdO^ [hZecna![g9dkf`"[ԕelcgb^\Z![ׂcf`e`][Z"[~MdJb_\Z$[kbka_[Z%[f`g`^([ gcDd`])[ yhalc`]*[ ue^hc`\+[ }dcfc`\,[ dbda_\-[ Pcfa^\.[ sgqb_\/[uld_\0[|Yc`]1[Z`e`]2[ddf_]3[fp`]4[oha]5[njYb^6[\kad_7[feff`\7[jVjc9[epf]9[lfgb:[k`fbZ:[ai_<[V^\<[fc=[]\[Z[) .j')m%-U#=!A  22H k!S#g%i& n%z&&*5+ E, W- i. k/ a0 R1 A2 023 45r6[7<8 8W99j;1<(==/>[~}u/􋊋-v+ ) 􍊇' w'|&d}%w#{"S~!}    !"\`"kx#Y}#$'}(u*,+}+*{ +kuz + +y} ,vt -z /P /s 0t322c|23r|4}4t|5667}77~8{8}:q9::~x:w;p<~H<<<</P-ҩ+ ) ' ֩'&%#"ֻ!  " !""#$Է&')+++** + + , - . / / 0422234456ֽ67888899;::;<׷<<<=[]_^]ah^im/[\]\^a`YgB-[\]]eenbg+ [Z[`aegKab) [Z]abecZa\' [Z^ba`_y]'[Z[]^_^dib&[^buagc%[Z\ae^nj#[\]aOkhn"[]ahff![]bj\ed []`cf_\ [\__\[^[\[[\r![b [Z\\![S\J"[{Z{"[]tYn#[]|ao#[\]Idg$[\]Xfd'[Yh[([Zn`*[ed,[i+[^j|+[^_`*[\_efc *[\_ctno +[\`hcd +[Z]bkdj ,[Z]co^s -[Z]bl` /[]bf /[]`c^m 0[]dqh3[\]2[]^k2[\awh2[\afej3[]dqjm4[_bPh4[\aoi5[^ca`6[]ea6[]_^]7[\c^e7[\]\d7[\^h{8[^lhh8[\`^i{:[rhv9[\aad:[\Ocm:[\ifl:[]dbf`;[]fe<[h_<[^aY<[\bd=[bd=[bdⱡR 4|Q3f30 Մ</ R-@ ,+?)/(G'p %>!ʌC a!Y PE ?!<!C"O#P$N#K$I'F'D&B';("+x+,,-[.8 -& 0 0G 1 2' 2Z 3' 3~4Q55I66m77j881899::L:;;";: <$7#4"2!.!) "1 ? !H "M #M $I%>&-'()*+, - .2 /B 0K1O0P1O2K3D47589:;<=>@>>====d=|<<$7#4"2!.!) "1 ? !H "M #M $I%>&-'()*+, - .2 /B 0K1O0P1O2K3D47589:;<=>@>>=====<<$7#4"2!.!) "1 ? !H "M #M $I%>&-'()*+, - .2 /B 0K1O0P1O2K3D47589:;<=>@>>===c=`$=^Z< ?=&<< ;J :R 9U 8S 7N6C553221 / ). 6- ?, E+I*K)N (N 'L &J& ?=&<< ;J :R 9U 8S 7N6C553221 / ). 6- ?, E+I*K)N (N 'L &J& ?=&<< ;J :R 9U 8S 7N6C553221 / ). 6- ?, E+I*K)N (N 'L &J& =7=4P<J^<LVX;/VRG:UGdY:BbWU_:dY\a ==r;m<=:;f9?Ǵ:8AƳ< ==F;Df<#^x:bnp;>pj\:$n]r:Uzpnz:rv|>= =;:9 8654eEX2$Z^_e?3TIX`aI]aZ\- $OUZ__`Y\_a_^.FR[]bm\[[`^^`a+DY\`f]Y[`_^`a) ;]\`ZW\_^]`a'7U]vZY^\^` a&CYPa[j[\_a$K{V__]U\`a 2XZZ[_U]`a/KYV[bX_a`aRs\kY_``a E][aZ_``a_WS]Y^``a Pp] ]``a7[Zd`^_a->g^Y^`aJYVb_]` a$YZ\K`_`!aDa]b[Z$aSU^^]`$aTYgN^_`%a0^YT\(aUO^X]`(a 3\V_[^`)a >]Z_]_`*a E\Za^_`+a ;VU`_`-a :VRc^`.a 8TQb[_/a KPg\_`/aIRc\_1a0cV^[^2a%[Z\Y^`2aVXXY\`3aSU_W[`4aPR]UY`5a LOVRW7aEURMW8aEKY9a4ea_`9aCTYZ]:aOQLZ;a=[_;a^\_a>= =;:9 8654䫕2 ʆ/M‰¶-!P´.8ټ+7Ȱ)"|Ĺ's˳ #¸ !0﮿ idIJ@ճL´!ܺ>sƾ_}ϼĿ O!Ļĸ$$A˜%f& ( m( ) * z, zż.vķ.j͹/ƺ0mĴ1Q234+5!789n¾9::;<==}>>= =;:9 8654w^p2 syzS/&hfp{|\w|tu-0fnsy{{rvz|yy.!Zitx}wut{yy{|+Xqwztruzzx{|) M{uyonuyxwz|'Gnwsqzuwz |& Use{utuz|$`nz{wmv{| Aqstuynw|=arnt~qy}{|&ivrz{{|,Ywu|tz{{|{pkxrx{|hw!w{{|Gusyxz|;Oxry{|`ro~zw{ |/rsua{z{!| W}w~tt$|loyxv{$|&lsdyz{%|@xsnw(| lgxrw{(| Cumyuxz)| Pxrzvy{*| Ywu|yz{+| Lmm{z{-| Mojy{.| Jmj}uy{.|)`fvyz/|Wi~wz{0|Aqzuy2|0ttutx{2| oqtrw{3|knzpu{4|fjvnr6|bfpkp7|Ymico8|Y`r9|D|z{9|Wlrsw:|ehbt;|Ouz;|wvz<|py=|vz=|y~|}>|z8|9:;<= cRWZ]^_`` a`_^\YUNvCTY\_``bPX\^_`` a`_][V@ia`_]ZUO\gV[^`bSZ^_a_]]^__`a`_^^]^`a^[VCl_`]XMߗ]_bX]_^\\^_`aa`a`a`_]\]`_[Me`^ZTx[`^\]`a`__``#a`_`a`^\]_]Spa`]^``__0a`_`a`]\^`Y`8a`__``^>a_ az8|9:;<= N Ԕ D ¾ؾ콹¿&¿19> @z8|9:;<= t-jqtvxyz{ |{zxwvrme[lrvy{{~hruwyz{ |{ywvumS|{zwsnepwnuz{}jsxz|zwwxyyz{|{zyywwx{|yvoYy{xqevz}qyzyvwyz{||{z{|{|{|{ywvvzzvd|zskszxuw{|{zz{{#|{z{yvw{yj|zwx{{yz0|{yz|zwuwzrz8|{yy{{x=|{z |Uc .D .R . . . .#.". . . .'. . .%.!. . .$.%.. .'.!. .%.%.!. .$.*. ..(.$. ..'[4󋊉-􋉅*ꋌxq(鋊騥'vm‘}&q|$z#{~ yu}݊u|zT㔈fЃɅΆɅȂɅφʂǃ͆̆ȃʂ͆ˆDŽɅ͇̅ǃʅχɇǁ~ɇχɃȃ͆̇Ȅʄχ·Ƀ5.-)'I&$#ּ[Z9[Z[]\]\4[\]^\[__\]_^\[\Z-[\^bb`Zcc]`fb^_[\*[\[bclqP]``\^b`]\fcf_^)[\]\^fgfk蝙h\_acc^'[\`b^npjb\%[\asdaejY$[]akhWR#[\_kemI[\ []`^f[f0][\][auoac_[]apfdue][\`Eo^ce`[]`cb}Oaa_[^bif9_a\[]a~f^\[\^^c_`^[]`\ndfbZ[]_^cda[\aa_[\]^^bc`[\_a`egb[\]bc`[\]\ab`[\^`_egbZ[]_a`dfa[\]\ab`[\]bc`[\^a`dfbZ[\^`_efbZ[\]\ab_[\]^^ab_[\^a`cea[\^`_bc`[\^]`a_[\^]cda[]_a`dfa[\^_^bd`[``_[]^`_`a_[\_bafhb[\]_^`a_[``_[\^`_dfb[]_b`dea[\^]aa_[\]]`a_[\_a`deaZ[\^a`dea[\]]``^[\^]`a_[]_b`dfb[T0Ţ1  $y,^3 b' 7x$S$wQ"Z j*;[w {EE&^FO"!O'!E !F!O!J !3!H!Q!:!>!L!M!=!;!N!L!:!C!S!F!;!A!R!L!1!O!R!<!?!L!Q!@!>!P!M!<=>>>>> <<;;<:::;;=(=>>>>> <;<<<;;;;g=[`a>[a>[a>[^=[Zb>[\~[Z [\<[\]]<[\^^<[^_];[\`_<[]b_Z:[Z]eaY:[Z]eaY:[Z^eaY;[^b_Z;[\^]=[\([= ==[~>>0=3==*=6<=<J<<4<?<@<E;T;{;;!;Q;W:3;>;U:S:M;T:Z:[:\:Z:[:\:\:\:Z:[:[:Z:[:Z:Z:[:[:Z:Z:[:[:Y:Y:Z:Y:Z:[:Z:Z:Y:Z:Z:Y:Y:Y:D%8$&#""!(;G M  P !O"O#M$K%H$D'=(6)+)!+ ,+ -< .J /S 0[ 2] 3Z 4S5H676 789:;.~D%8$&#""!(;G M  P !O"O#M$K%H$D'=(6)+)!+ ,+ -< .J /S 0[ 2] 3Z 4S5H676 789:;.~D%8$&#""!(;G M  P !O"O#M$K%H$D'=(6)+)!+ ,+ -< .J /S 0[ 2] 3Z 4S5H676 789:;.~ >><O;V:Z9\8[7Y6S5I474324 0 / 3. F -R ,\ +_ *` )\ 'T&I% >><O;V:Z9\8[7Y6S5I474324 0 / 3. F -R ,\ +_ *` )\ 'T&I% >><O;V:Z9\8[7Y6S5I474324 0 / 3. F -R ,\ +_ *` )\ 'T&I%8L^W`a8H^T]aa7.T`KZa6ZYY^a6+ZS[^ba5$Pb[_a3K[X\_a4'KdZ_a2H]\^`a3(ja[_a17\Y]_a2"^_^_`a2%[T]`a1Y]]_`a11X,]`a1X\Z^`a1Wp_ a05ZW]` a0y` a0Oh^` a/2ZW]` a/Q_`` a-Jo_ a.:[Y_ a.U^_` a,Lt_ a-9[Z_ a-O__` a-ba-VK_a,#[Z_a,O__`a,uba,Ua,ZX_a+K^^`a+Oa+Gga+WU_a+[`a+]`a+_a*N`a*ca*TDa*WVa*Za*]\a*_^a^*a*aG*aY*gaU_*a\a*FaW`a*PaT]aa*SaKZa*U aY^a*V a[^ba*W a[_a*X aX\_a*Y aZ_a*Z a\^`a*X a[_a8/8n7b6C76\5Oõ34UǴ23U¶1q2M0L11g}1/޽ 0n0 0н /k / -۽ .w . , -w - -+,O,,,,+++++++************<**** * * * * * * 8"bxo{|8Qxkw||7=lybs|6'"srrx{||68sfty}|50c}sy}|3`tpuy|43_sy|2]wvxz|34|ty{|1Gvrwz|2.xyxz{|00umwz|1 swxyz|1@sGw{|1qvtxz|/py{ |0Etpwz |0|{ |0fx{ |/Btqx{ |/gz{{ |-\x{ |.Kusx{ |.nyz{ |,bz |-Jusz |-dyy{ |-}{|+n]z|,0try|,fyy{|,}|,l{|,rpz|+`xxz|+f{{|+Z|+pnz|+tsz|+x{|+z|*e{|*~|*kX|*pn|*s{|*w|*z|x*|*|\*|r*|nz*|v|*Y|o{|*g|kw||*k|bs|*l |rx{||*o |ty}|*p |sy}|*q |puy|*r |sy|*s |vxz|*q |ty{| ?~>=3<<W;:y9998\717 t6?6 l?a?:aI]aZ\8a`Y\_a_^5a\[[`^^`6aY[`_^`6aW\_^]`7aY^\^`9a[\_:aU\`:aU]`:aX_a`9aY_``6a]aaZ_``9aY^``9a ]``7a[aa`^_:aY^`;a_]`:aK`_`:a[Z7¾::::<:::;:::;<;;;;;;;;<<=:µ<<<<==}?|S:|\w|tu8|{rvz|yy5|wut{yy{6|ruzzx{6|nuyxwz7|qzuwz9|tuz:|mv{:|nw;|qy}{9|rz{{6|w||tz{{9|rx{:|!w{{7|u||yxz:|ry{;|zw{:|a{z{:|t<|xv{:|dyz{:|nw<|rw{;|uxz;|vy{;|yz{;|z{<|y{<|uy{;|vyz;|wz{;|uy<|tx{;|rw{;|pu{;|nr<|kp<|co<|r=|z{:|l|sw<|bt=|uz<|vz<|py=|vz=|y~|}|?:N8Gbg57Mq3#;W0$O .E - v +)':%A !uI6%F* %"L#$$& M'(*(4+=+D,J- I, Q- L. C/ 83.4(5$6!78c9G9F:,;y<V<:=>?OaPX\^_`` a`_][V@aSZ^_a_]]^__`a`_^^]^`a^[VCaX]_^\\^_`aa`a`a`_]\]`_[Ma[`^\]`a`__``#a`_`a`^\]_]Sa^``__0a`_`a`]\^`Y`8a`__``^>a_aO 󥵽 ¾&¿19> O|hruwyz{ |{ywvumS|jsxz|zwwxyyz{|{zyywwx{|yvoY|qyzyvwyz{||{z{|{|{|{ywvvzzvd|szxuw{|{zz{{#|{z{yvw{yj|x{{yz0|{yz|zwuwzrz8|{yy{{x=|{z|O$0368:: ;:752,&)*9Z pG/)'!-R~ʵh># E'f)u2ꨄ~c0:wfV>a`?a[^_a^Z?a_[?a`\<=a`^)=a`_]>a`Y>a_Vaa`:a`_][=a`\Fa_;a^[^=a`_`>a`W3=a_RV=a^Wl=a_\I=a`_Q>a_UaT3~a_?a\Y=a_Z>a`^a_a_Y=a`^>a`Z=a`_>a`\=a`_>a`@a<<<==?;ý¨9¿:¿:¼:<¼?=y=R=>=¾:=¼;==`<==<><ü=<û;¼;½;½;¾;¿;½;½;<þ<ÿ==m=>==>¿<==><>====~<|zsw<|{y<|}|v=|{z=|}@|utr<|yzxr|q;|yw{xpw<|yv{wsn<|xvxvl<|{wtu>|yt?|zu?|zuN=|{x:=|{zw>|{zr=|{yzp||y:|{ywr=|zu\|y;|yux=|zyy=|{zn2=|xgm=|xn=|xv_<|}zzh>|yl<|}xnP=|wqb=|wrj;|}|xtm;|}|xuo;|}|yvs;|}|zvq;|}|yvo;|}|yui;|}|yra<|}yoT<|}yk=|yg`=|{ig>|lB>|}>|y{>|uq=|zs>|zy|z<|zx=|{xg=|yv=|{zt=|{y>|zs=|{y=|}|v=|{z=|}A|(<UC 9847+" 4p# 221 K0 k.ό+Æ)_(ޚB&|!_".c ? ܈"###=$T'k (z)*+"* +. / 0 1x 2X 3=346 771899p :E9;k<.==E>>=0*儂 ]t)с~ _Yr)Ձ LZ)򄃆 ]_\w( aZ`'с~ `]Yj'僁 a`\`d% a`]UZk&ہ a`\Y$؁} a^VYQ$a`^Y\$a^VMO#ҋa_^\#a_VD"a^[YY"a`[e"ыa^ZJU"ۊa_^]]"a`_a!a^YGU!Պa`_^_ a`a__ a_[IV 눈 a`_]^ф a`_ai a`]LX a`^\]ۄ a`\ہ a`b a_ZH a_YLRꂀ a`]YZqЂ a`a년 a`^sC a`ZDOт a`\VXނ a`__^ a_dڅ a^OVԁ~ a^Z[Ӆ a_]] a`__탁 a`kρ~ aby탂 a`zR a^PWׅ a^XZ҂ a_\]ۅ a`__ a` aЃ a a񇆈adыab>acCLac;]ahBaZ atF`] a`Ha`\ a_Ia`]U a_Ha` atF* ) ) ) ( ' ' % »& $ û$$ü##þ#ý"ʏ"ý""ž!ÿ!          ÿ                      wĎƏ Έ   »  .*cea\ [v)ehb\ [zs)egc\ [btF)cda\ [wzt(egc\ [zt|'fhc\ [{wr'dfb\ [|w{%dfb[|{wot&egc\ [|{yur$fhc\ [|wnql$_`^[|zyqu$\[|xm`h#]]\[|zxu#\[|zo`"[|yuts"\[|{zuE"\[|xs_n"]]\[|zyxx"\[|{z|2![|ys\n!]]\[|{yxy [|zyw [|zubp __^\ [|{yxxcea\ [|{z|ceb\ [|{wapbca\ [|{ywxceb\ [ |{iehc\ [ |{~ceb\ [ |zu^bca\ [ |zsbjegb\ [ |{vqs{egc\ [ |{z{bc`\ [ |{yXcea\ [ |zsWeegc\ [ |{umpegc\ [ |{zzkbca\ [ |zbc`\ [ |xfofid\ [ |xrtbca\ [ |zwx`a_\ [ |{zzdeb\ [|{xfhd] [|}deb\ [ |{iab`\ [ |yfnbca\ [ |yprehc\ [ |zvwbc`\ [ |{yyab`\ [|{egc\ [|dfb\ [|bca\ [|`a_[|r]]\[|}V[|X[b|O[w|T[zt |{Z[{w |z^[|w |z_[|{wo |z][|{y |z[[.). ..*.". .%.(.$..*.3.....3.2././.3.3.,.1.4.*.$.&.+.&..&.+.!. .,.).#..+.,. .(./.%. .'...*..*... .%.,.1.>.C.E-Eb-E,E,E"+E *E*E[ɅΈ̆ǂ˅ΉɇȃʈΈʄz~Ȅ~͇͈ȆɃЈ·ǂȆψˆǂ͉͆ɆȅˈΈʅȄ͇ȆȃˊψȃɅωˈǃ̇Њ̆ȃˈωɅȃ΅ˆǂǂ˂ˀ{~s~g~}Ą{Ά[\^`_bc`[\`a_[\]^]ab`[\_cadfa[\^`_cda[\_`^[\^__bb`[]_baegbZ[\^`_ab_[\`a_[\^`_cda[`flhefbZ[`digbc`[\]`_``^[\_a`bc`[]`badfb[\]\__^[\]]``^[]`dbegbZ[\_a`bc`[_`^[\]`_ab_[]`dbefbZ[\^`_ab_[\]^^][\^a`ab`[]`cacda[]^`__`^[\]__^[\^a`cda[]`caefbZ[\^]`a_[\]]][]_caaa_[]`badea[\]_^]]\[\]__^[]`cbdea[]_bacda[\[__^[\^a__`^[]`dbdea[\^_^aa_[\]^][\^`_ab_[^`dbdea[]^`___^[\]\__^[]_bacda[]`ecdeaZ[\]bc`[\]_^ab_[]`bafhb[]`cafhb[\]^]egb[\]^]fjb[]`dbiqb[]_baiya[\]]i^[\^`_jY[]`dbq[\\[\^a`c^^][\]_`^[\^`_ab`[!G!S!K!>!D!U!H!:!O!R!C!A!L!S!F!;!V!Q!:!K!S!N!=!I!V!G!B!P!S!G!D!N!U!I!>!V!Q!@!K!U!S!@!I!X!J!B!S!T!I!@!K!N!/!.!4!!z!i"R"C"@"?"="5=&&&&()*<<<==6='&&')*+<<=>>6>[\^&[\[\^^&[]^\[\^^]&[]_][\]]\&[Z]`][]\([\]\[\)[Z[^][\*[Z]a_<[\__<[Z\_^<[\^_>[^]=[\]\[8t8}2y{-%Xs"y|  z~nq}zz$u }% }( }+p+py21o1󍈅p~3x6͌6~x8::9<>>882-%"  c % % ) +.2123769::9<=>[]^d_8[Z_bpd8[^`ib2dda^[[\\`]-c`_]_``_^][^_e`%oXb^[_]\\^]\[Z_bqd"edc^]]^]\\]\\[Z[]^f` `_`_^\[Z [\`]ea_^]\[Z^`jbi^a^]]\[[\[Z`bodb}]^\[]^a^^b]_][\[\]^]`aa]^]\\[Z_ahbda]_]$[Z`bnd c^``\]\%[]^d_ _c_\]\([\_] b\\^^*[Z`cue^_`[\+[Z_bmcd\[]\0[^\^a`\1[^_f`g[\^^1[Z`cteab\[\3[_amb^_^\6[\_\[\]\6[^_id]9[Z`ca`^9[Z^^_^\:[\]\:[Z\<[]>[][Y:Y:Y4Z/ .50>HY' %9@HWjX$0Hw{Y" !&" ޢ~;Q- vH'f@*|X) ,ͨzQ* /΄_@1ݣ\)*4Â<5zO7q9Ϥz4:ݪ=> =$5#6":!@ EKNQUV Y  Z ![ "X#P%F&5q$"$ ]j"~""!         !}"$#  }s$s&&''{((((=$5#6":!@ EKNQUV Y  Z ![ "X#P%F&5ر$"$ ֓"""!         !"$#  ó$ֳ&&&&ҽ((((=$5#6":!@ EKNQUV Y  Z ![ "X#P%F&5[I$"ZWU$ [ZYZ:D"[ZRU"[ZVX"[Z[![ZW [YZ [ [[[[[[[[[[[[[[[[ [ [ [  [  [Y [XU![VP"[X$[#[ZX [Z[ZWT [Z[ZQI%[ZI&[Z&[ZX'[XT'[ZXO([YW([YTS([Z([Z> =uQ<z9ۋE8ȧ/736D#4g32 1 0/.-,+*)('&%$#"! X C!3 .  ; %~%z&?&Z'&'8(^'a*W aY]_a*V a^_`a*UaT]`a*Ra]_`a*Ia,]`a+aZ^`a*ba_ a*aW]` a*a` a*^a^` a*Y`aW]` a*UT_a` a*PN_a_ a*KB`aY_ a*Ea_` a*]a_ a*E__aaZ_ a*3[[`a_` a+VU_a+QN_K_a+MB_Z_a+Eta_`a+_a+=_a+[X_a,V^`a-a,Pa,U_a,[`a,]`a,_a,a,a+Da+Va+Za+\a+^a+a+a+a+a+a*Fa*Pa*Sa*Ua*Va*Wa*Xa*Ya*Za*Xa*Wa*Va*Ua*Ra*Ia+a+a+a+a+^a* * ***}+* ** * * * * * * * *´ *k¾ ++++++}+6,-,,+,,,,++++++++++***************+++++*o |rwz|*n |xz{|*l|mwz|*g|xyz|*_|Gw{|+|txz|*~|y{ |*|pwz |*|{ |*yx|x{ |*r{|qx{ |*lkz|{ |*fcz|x{ |*aU{|sx{ |*Y|z{ |*x|z |*[zz||sz |*Cuuz|y{ |+omy|{|+hey]z|+bSzry|+X|y{|+y|+Nz{|+upz|,nxz|-{|,f|,nz|,sz|,x{|,z|,|,|+X|+n|+s{|+w|+z|+|+|+|+|+|*Y|*g|*k|*l|*o|*p|*q|*r|*s|*q|*o|*n|*l|*g|*_|+|+|+|+|+x|5D5 r4@4393 31 2 2, 2C 1" 19 1Y 0+ 0M 0n /3 /f /y . .P..--K---,<,~,,,,+ +9+z+++++++****0*G*_*w***d*K*6*"**+++++yaO\`;abVK^bac>ab a;<;í;<ö<Ĝ:ù:Ƨ;ÿ<ĸ<ų<û=Ĺ=>> y|fu{;|}nax<|{`u<|}thw<|}cu}}:|}vow};|jjv<|zdn{;|}utz<|~rs<|}xw=|}u=|}{>|~>|} |==F<=;2;): 998/6765Z54z3 3 2 1 1s 0% /! /G /. .;,++9,3*+@)t(H(=)F'8'>'*%L%F# $\"#u# ###########$$-%%&& ac`Ycae`Zcaf_[dadfYcadcYdae`Zcae`ac`ad`afaZcaeaZcadbYcab`aeaZdaXaf_\dab`Vae_aba[Zad`a`[U[ba`d`Zcab_V[baf`Zca^]_a`bad`ab_Y^ba`badaZdab`^`aa[_bae`a^Z`baaYX_b af_\daabZQZa\W_b ad`ZdbacXQ^ba`YW^b adaZdba[U\acZQ[c afa\cabWTabab_Z[ab ae`b`\^ab_XZ` ac`_Z\bab_VZc ad`XY ab`[] af`]^ a`XZb ae`Z a]^ba`cfZa_a_\ ]^e{ i\`a `^[X YXZ^`a`_a²þý¶ü³ ĵ¸ Ų ķŴ ůĿ ÿĿ ľý į º ò ľ  ƽ  |{gh{|yhg|yhg|zmg{|{kg{|zhg|zhg{|{hg{|zhg{|zhg{|{hg{|{ig{|}{|zhf{|{q|yhez|}{n|zhf{|}|ts|{hf{|{tlu}|zhf{|}znu}|yhg|{xwz|{}|zhg{|}yqx}|z|{hg{|zx{||tz}|zhg{|xt{}||rqz} |yhg{||~tis{|vpy} |zhfz||~rhx}|zrox} |{gez|}umv|}shu |zgf{|~pl|}|}zsu{} |yff{}{vx|}zrs{ |{gf{zsv}|}yos~ |zgf{pr |}{uv |ygfzwy |zqr} |zhdpv} |x} |{m}ewz|yv wtv {tz|{z {zxu vuv uvutvz{|{|5͂5P9;<=>v ab>a_X=aWO=aVTcab ?a>==<<== ?|}>|zq=|pf=|nk~<|ny}<|p=|z>|~ ?|@l<`=Y;f<D:~9:#:9776-65H54`4 \3 w2 a2 n1 V/ v. i/ ,- w. ,+}, ~, g*{+U)()/'')''''''''''''(5),*:*C+&+a^V ag@a`^ ad6a^V aha_^ axa_V am a^[Ya`݇ a`[a`]^plt a^ZJa^[\a^cv a_^]a]XY`dv a`_a]SUa`cwa^YGa\JPa_cwa`_^a],Ka_cxa`a_zEa_cxa_[Iaca_cya`_]aa`Va^cya`_aa^[\a^cya`]La\UW a^cya`^\`[LQ a_cx a`\*K a_cw a`a_h> a_cw a_Za a_cv a_YLaaa`cv a`]Yaaa`d a`aaa` a`^aaa`ZDaa`\Vaa`_aa_aa^Oa^Za_]a`_ a aa`a^Pa^Xa_\a`_ a`!a!a!a!a!a!a!a!a!a a` a_ a_!a!a!a!a!a!a a`a`]a^[a]Xa]Saû ͇ yü  þ y ý  ý  ÿ`¾  [ ¿у  ÿ¢ ¸ ¹ ¹¹º³ !!!!!!!       !!!!|wn |S[|zy |I[|xm |s[|zx |Z[|zo |k\[Z [|yut|{zz`a_[Z [|{zu|zwxtvq^[Z [|xs_|yuv|}{p^[Z [|zyx|xqr|}}{o^[Z [|{z|wkm|}{o^[Z[|ys\|w`h|}{o_[Z[|{yx|x;b|}{n_[Z[|z|zY|}{m_[Z[|zub|~|}{m`[Z[|{yx||{b|}|m`[Z[|{z|{yvw|}|m`[Z[|{wa{vmp |}|m_[Z[[|{yw{uah |}{n_[Z[ |{v7a |}{n_[Z |{|zQ |}{o^[ |zu| |}{o^ |zsb|||}{p |{vq|||}{ |{z|||} |{y|||zsW||{um||{z||z||xf|xr|zw|{z | ||{|yf|yp|zv|{y |{!|!|!|!|!|!|!|!| |{ |z |z |z |z!|!|!|!|!||{z|zw|yu|xq|wk| )E$)E~ (E,(En 'E0&Ei &E)%Eh %Ex %E:$E{$E$EH#E#E"#EU"E""E0"E4"EP"E!E:!EG!EU!E ED EY Eb EEDE`EoEsEtEEEIEpEEEEEEEEEEEEEEEEEF>52333}3`3?==<;:9876543210 / . - , +*)('&%$#"!  !"#$%&[Z?[Z=[^[Z<[p^[Z;[{p^[Z:[}{p^[Z9[|}{o_[Z8[|}{o_[Z7[|}{n_[Z6[|}{n`[Z5[|}|m`[Z4[|}|m`[Z3[|}|m`[Z2[|}|n_[Z1[|}|n_[Z0[ |}{o_[Z/[ |}{o^[Z.[ |}{p^[Z-[ |}{p^[Z,[ |}{p^[Z+[|}{p^[Z*[|}{p^[Z)[|}{o_[Z([|}{o_[Z'[|}{n`[Z&[|}|n`[Z%[|}|n`[Z$[|}|n`[Z#[|}|n`[Z"[|}|n_[Z![|}|o_[Z [|}{o_[Z[|}{p^[Z[|}{p^[Z[|}{p^[Z[|}{p^[Z[|}{p^[Z[|}{p_[Z[ |}{o_[Z[!|}{o_[Z["|}|n`[Z[#|}|n`[Z[$|}|n`[Z[%|}|n`[Z[&|}|n`[Z[{~tՁ~|r0ɂ u   ꂀ }!_!!킀!{v|"x""󆅆"uu#ˈ#}}#n}{$s$܈$}v&&yp'|k'}'s(~(W(傆)})y *}*v +r +򋉈 ,z ,Ljv -t -} .x / / 0} 1ي 2 3 4 4 5 5 4 4 4 5 4 4 5 5Í      !!!!""""##$$$%&&'''((ܾ()ϭ)** + + , , - - . / / 0 1 2 3 4 4 5 5 4 4 4 5 4 4 5 5[]aechld[]_b`ipc[fhc\[]`dblY]\[]afcR[\]^]bca\[\]^]fgd\[]`dbfhd\[^`db fpb[\]^] k\Y[\]_^ _\[]`db egd\[]_ba fie[\]!\X_[\^`_!_]^[^b`!efd\[\]]!imi\["cf^["_^_\["aba\["X1o][#`^a\[#i^[#Rhj_[$go][$tcb][$hn^[&fca][&kqa\['ita]['jee_\['p2`\[(m^\[(R_\[(wbd_][)kb]Z[)ncha]Z [*xic^[*k\a^\ [+tap]\ [+a^_^]Z [,kZc_ [,3_n`\Z [-p`a^[Z [-jj[c_ [.end_ [/f^db [/eada [0i`N [1]\ [2Z [3 [4 [4 [4 [5 [4 [4 [4 [3 [4 [4 [5 [5"" n"$#0#,#&###K$$$$ u%% &%u>%JW&]&]&V]'](](C]('](])Q])]*?]*]* ]+/],d],3]-H]-+]-f ]. ].Y ]/$ ]/E ]0n ]1+]1P ]2z ]3 ]3V]4']4 v]5]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6 >>=====<<;:987777 >>~>>=<<;:987777 [\>[\>[\=[\=[\=[>[=[W<[<[;[:[9[x8[b7[7[7[k7[ ==<;;.:9 98%7*6,5-4 03 ]2 *1 0>}=>><={==}=|}?==?>>>=~>}<=;;<=<<<<==|}>==?>>>=<? [Z>[X>[Z<[ZU=[YV=[Z|[ZY=[Z}[ZY=[Z|[Z}[W~[ZY~[Z>[ZX>[Z>[Z>[Y[>=ޜ;<=;<^=;==   <<;;;;<;:;<<<<>;;=;<֛=;===4[Z[Z [ Z[ ZYWWXWWXYX [ZWWZYZ[ ZSUTTSVQRVSTU [ZY[Z[YV<[ZXT=[ZX;[ZW\<[XWT;[ZX<[ZY<[YWT;[ZYVT;[ZX;[ZYX<[ZXW<[Z<[ZY=[Y>[W;[ZYZ;[ZYX=[ZX;[ZY=[ZYI=[YW{[ZY=[ZV[) ח}{{xuy 8           Y 2 r2 2.3%3R 4m5G55267S7s8Q8<9J99h:z*:Q;8;[ ;<u=0=(((j(&a%z$a#aa  ~a񇈈axa݅_aaar|a}ilf``a }\^d_`a 􋇐oqrid_`a }nee_a 󋊒w{ugb`a 􋊉~{dcaxhvca􊈋uw}ca}sob atbsc` a񈄇|~|e_ aqj]azz|o_`aT~~e_a}fPj_aYglg_akxpf`agaxvqakuvj_aarwwe`auwwd`ptvcdotwp    !$p""%##%$W$$x&M%x~%&z'X&q|(((ӝ(&#ȼ%! ºǵ= ʳ غ   ѾŴԼ)Բ  ƿ٠峢֦׶ҹڴκҿҳd  !!#ڰ""%##%$׃#$Լ&z%ֻ%%'&ױ([Z([ZWU([YX?([ZY'[ZX|&[ZXSM%[ZXVU|#[ZYXUU||$ZWQS|XWXWXWWXXWXXWWXWYX|M|VUUVVUVUTTUTTVR~|W||VTTUVUTVVSMR|[\^_{jpz~}| [\^QZV^oty~| [\]X\[``mw{}}| [\[^\\_`dpwx}| [\X\\^^a_iy{}| [\]`a^^btz|[\[^`^\Xbdd{|[\^\a_\^b`hb{|[\^b`a[\Y{\_j{ |[\^1d\[\aid_fcy} |[]_^\[\^`Yacy} |[\_^_[^_gbaw~|[\]`^[`c_`bp{}|[\^\b\[^a7N^hw~|[\[Z[^KW[env~|[\[]][^_JSdjv~|[\R]\[]JWd`iw}|[\X\[\efabk|[\[[\[Z_ebdt}||"[Z^dbfx}|$[_dcey}%[^eccz[Z[Z[^gdb[K[_scYaX[ZG[][Z [[Z [ [Z[[XZ[ [ZX[[ZYhV[[ZR[ [Z\b[ [ZSYS[![Z[\["[aY["[ZW[$[F[#[W["[ZYY[%[X[$[dW[$[Z[%[ZT[$[Z7S[$[YTU[$[ZYXN[[&[`/[[%[YNR[[%[ZVV[[&[ZYP['[^>[&[ZIQ[(^(N'3&i&D%#/Š(rKwtuwttuwvx|{~~~xyzustrj`:,     ]+Y`a+T_a+N_a+B`a,a,a,_a,[`a,U_a,N_a,B_a-a-a-_a-Z_a-O_a.a._`a.W^a/]` a/`a/`a/Z_ a0_ a0a0` a0X_ a1 a1^` a1S_` a2 a2_` a2U_` a3 a3fcba4cba4cba4kcba5cba5tdca6dcba7gda7fcba7dca7ifda7biea7beda7bofbaa7bcaa7bgcb7bfb7bc7bf7b7b7b7b7b7b7b7b7b7b7b++++,,,,,,,-----.../ // / 0 00 0 1 1 1 2 2 2 3 3 44 4 556777*7*7*7*7*ൿ7*7*ٷ7*7*7*7*7*7*7*7*7*7*7*7*7*7*+r{|+kz|+cz|+U{|,|,|,z|,uz|,my|,ey|,Sz|-|-|-z|-tz|-ez|.|.y{|.py|/x{ |/{|/{|/ty |0z |0|0{ |0qz |1 |1xz |1iz{ |2 |2z{ |2kz{ |3y{ |3vy{|4x{|4} |4qz{|5wz{|6tx|6xyz|7mu}|7sz{|7 qv|7 _ty|7 >vx|7 >^lv|7 >^Vq{||7 >^[[x||7 >^[[gy{7 >^[t{7 >^[{7 >^[k7 >^[7 >^[7 >^[7 >^[7 >^[7 >^[7 >^[7 >^[7 >^[7 >^[7 >^[+++K+,,,,,,U,----d-...O. /w /m /N /} 0X 0Y 0 w 1K 14 1a 23 2 2W 3" 3 n4*4 l4:5Q5+6C7"7;8 8,9W: :P<<=>>/ ab>afc=adb=>ٽ<;:::8765 |y>|sy}<|[y{<|[n|};|[sy}:|[vy:|[kx{9|[vz9|[y{8|[y{7|[Jwz6|[Luz5|'6' )k)8(r*<)+9*A+(-5,7.8 .@ -K .d 0B 2 2\3- 4 3455677809 :Z:R9O:M;L<=?=>>==<;;t;8$9/887F6Z5ab8abab 8ab8ababa; :;|{;|{ :|{;|{|=<;9~I8 {_KR3QRJz=a|5*p*/+ x-,, . - N. s0 d/ r1 0 3 335s4n67O6 96839':2;,;;< <)a` a'()*+ , - . / 0123456789:;<=? '|}|n_[Z[(|}|o_[Z[)|}{p_[Z[*|}{p^[Z[+|}{p^[Z [,|}{q^[Z [-|}{q^[Z [.|}{p^[Z [/|}{p_[Z [0|}{o_[Z[1|}|o`[Z[2|}|n`[Z[3|}|n`[Z[4|}|n`[Z[5|}|n`[Z[6|}|n`[Z[7|}|o_[Z[[8|}|o_[Z[9|}|p_[Z:|}{p^[;|}{q^<|}{q=|}{>|} | 4 4 5 5 4 4 5 5 4 4 3 4 4 4 5 5 4 5 5455t5cu4`cu4a_bv5a_bw4a_bw4a_bw4a_by5a^dhg4a_^^4 a4 a3 a4 a4 a5 a5 a4 a4 a4 a5 a4 a4 a5 a4 a4 a5 a5 a5 a4 a4 a3 a4 a4 a5 a5 a4 a4 a4 a3 a4 a4 a3 4 4 5 5 4 4 5 5 4 4 5 4 4 4 5 5 4 5 54555445445544 4 5 5 4 5 5 4 4 4 5 4 4 5 5 4 5 5 5 4 4 5 4 4 5 5 4 4 4 5 4 4 5 [4 [4 [5 [5 [4 [4 [5 [4 [4 [4 [3 [4 [4 [4 [5 [4 [4 [5 [5Z[4[Z[5^[Z[5q^[Z[5{q_[Z[4}{p_[Z[4|}|o`[Z[5|}|o`[Z4|}|o`[ZZ4|}|n`[]4|}|mfh5|}{yy4|}4 |4 |3 |4 |4 |5 |5 |4 |4 |4 |3 |4 |4 |3 |4 |4 |5 |5 |3 |4 |4 |3 |4 |4 |5 |4 |4 |4 |4 |3 |4 |4 |3]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6]6[6;66666666666666678888999999999::::9:::::::88888:::9999999776k678854577744453235 2 18888899999999::::::::::::887788:::9999999766в678854477744443234 2 1Y7[8[8[8[R8[Z8[9[9[9[9[9[9[<9[X9[Z9[Z9[Z9[Z9[Z9[:[:[:[:[:[:[:[Z8[Z8[Z8[Z8[:[:[:[Z9[V9[9[9[9[9[9[9[9[Z8[YZ6[U@Z6[\Z7[8[8[8[YXZ5[T_6[7[7[7[Z5[YXZ4[VjZ4[_Z5[Z4[YZ3[\4[g5[ 5[ WSZ2[ G.-, +$) w(((((((Y(2''''''`&Y&:&%6%%e%X%,$$i$##h##","!Q  o)$ e!!}!3"c"#.$@%[%%i&' ($)b) *@+v,;=?>|=|>=>;=?=<>==;[ZY>[Z?[Z>[YN=[ZN>[Y>[X>[Z[:#:);$> Z8G4􊈇/+ꋈ;{ 󊋋č}~ x~ }zC^9<_=?===<='$$# z~v񋌍y򉑍򉍍  􋊌}J 򗈖~}  M=8ϐ1-+[  ٲ ־rԛ9<ї<?=<?<='$$"νζ˹  Ϗ   GZ[ZWZ9[YV?ZTZZ4[ZXW\YTWZYZ/[ZXWYZXTX[YYZSZ+[ZYYZWT"XYYZXRX[Z[O [ZWVsZYY[XPW[Z[Y[Z[YZb[ZVRbV [ZX[[ZZ[ZX[Z/[ZUQ^X [ZYVQ[ZX[ZPZ[X2[Z[ZYZ\YIZZXX[Z[ZXZWX:[ZU=[YX;=[Z@[Z=[Z[Z=[Zh[Z[Z$[Z[ZZ[ZY$[Z[Z%[Z[Z [ZYX[Z[ZY[ZVOY[YZ[ZY[YVQZVLWTU[Z[Z[Z[[ZXXYTMYSUXTYZ[Z[Z[ZVX[WV[UWZT[[\[Z[Y[Y_\YRT\WZ]W\[\[ZXSWZ[X[Y\[Wei^W\\W\ [ZWWTWZ[YW [Z[[W\[YPznU[V;WTW[ZUY[Z[cXb[XQZ[\YZ[[X[ZWRVVW[Z [Z[Z[Z[ZYZ[ZZ[ Z[>4>$=><n<F; ;9:n :V99,8چ%7V8X7J66%5t4Q4v4A3{ 3&2 b1 <1 k  7&!/+*45.182/42%'.+$(+!$,&&,,').&$-+#).0($2 񭩶ʼ̸Ŀ &')(]w''()*)))********************   %&'((())w**((()'&%s{&s'###{#%$}Ojz&&'('''(*)))))******************   %%&((())ع)*((((&%%ʳ&&"""ؽ#% xѝ̼&[YTU['[XY)[h([:M([ST([VW([XY)[Z)[\)[U)[X)[Z*[*[*[*[*[*[*[*[*[*[*[*[*[*[*[*[*[*[*[*[[Z [YVXX[Z[Z[Z[Z[Z[XY["[ZVY[%[YZ[[&[ZYY['[YX[([Y[)[Z([Z*[)[ZG*[Y*[([Z([ZWW([ZY)[Y'[ZW&[ZRS%[ZTIO&[ZX_I'[ZS'[XY[#[ZS[ZUU[#[YV[ZOS[#[Z[ZYXW[%[YSTQ[$[Z\d[[[Z[Z\RQ[[[ZY5ZYW\XY[[ZYUYYX?UXZ[[ZYWUM^WRT[4,2:4486//1$ +$%')"",-*.405;91//ѿο·ݯE- <% & '(?(V(](^(`(Q'#'&8%M%)$e#y"}$"4" !:  KLV~!@dv"7b7a7h78䁈757474747474777+8*689:;<?? 7*7*7+76757474747474777+8*689:;<?? 7 >^[7 <\[7IgYZ78@RVWY757474747474777+8*689:;<?? ndb4aldb3a jdb2ajdb1aidb0a kdb/a meb.avdb-adb,adb+acbb)adcb(aedd`&aheb&apeab`#acdc#anfc"afab a[ecaofcba`dcapecca(kecba.gccba-khcbba!jdgebbayfeebba fcecba!idedb a"oebdd a $lfbcda &mfacca (lfabccb *ifb - *),(2'4&2%+&%$#"! $ ,!)"#$%&'()54 3 2 2 / .-,+*)'&%#"!!ž(.-! !¾ " $ & ( * - *),(2'4&2%+&%$#"! $ ,!)"#$%&'()[Stz4| [uz3| Zev{2|XVWXVmw{1| qw{0| kvz/| auz.|sz},|sz}+|tz+|vz*|wy{}'|vwx}&|pux}%|r|{}#|syy#|hty"|o{{ |qxz{|rqy{|xvx{|Xpyz|(uvx{|.lxy{|-gnxzz|!ivmv{z|ursz{{| oxsy|!hytw{ |"Ku{vv|{| $bs{ww| &Xt|wy| (Nt|zwy| *lv{ - *),(2'4&2%+&%$#"! $ ,!)"#$%&'()t43 2 -1 40 &/ .|-Y,;))('|%e$8%$> ;k #z" I$^%)w&}( v+ g- V /(:o2BQv5 Ye8i|;OAacb;aifcabccb(ab aihfcababacec a hggfdba babefgghd ajgfefgfg ykfa2|qha2alea2egda2kqha2hqiba1dkea1hhea2mqiba2gqha2 a2gigca2psog`a2{~zqd`a2쌍va`a2슈g`a2pmqvuha2cbcdeca2efec`a2npmga2iliea3cdcba2gihea2u|sh`a2rwqh`a3puph`a2vth`a@<8   2222212222 2222222223332232@|{yy{;|isy|zyz{(|{z |epvz||{zz{{|{z{||zwz | ^krvz|{z{z{|zupkYhy{|BdhilmoprsuvutsqonkkhbM [ox|2hrx|2|uz|2zxz|2wty|2ysx|1{wz|1xxy|2vsx{|2zrx|2 |2xwx{|2truy}|2khls{|2[Z\^o|2^`^Zhy|2tvtopx|2{|{zz{|2y{|2truy|2wwxy|3{|2xz|2ojqx|2qnrx|3rptx|2ndpx|@<:8" Z͟S '>WrŰz]B( =3E444444444e4^4W4^4]4=4O4`4a4P4R4d4T454?4D4#a| abhkcacgkcabhkcabimeacgjaacgiaabhjaachiaachiaacgiaachkbachjdachiaachiaabgjcabfibabghaabgiaabgibabgjcabgiaabgjbabfjdabfgbabfhaabfh`abeg_abfg_abfh_abfh`abfibabfg]abfg`abfh^adfYade]aef\aegZ~aegZaef[adfZadfP}adeO}adeUabde=|ade{adfJ~abcTabe|acg{ad$|adE|abfyabfyabf{ijhbabg{ffeabhtphhfbabhspghfbabhqoTfaehjrileacsufedafrop{GkfadiUpovjbaeohs{wgca ſĿľſĿľĻĽķĨĹüĮłİĸĎϷƾ^̀ |yv{|{xv{|{wv{|{xvz|yw||xw||yw||yw||yw||xw||yw{|yvz|yw{|yw||yw{|yx{|yx||yw||yw{|x{|yx||yw{|ywz|yx{|y||y}J|{yy}V|y}U|y}V|{yy}^|yx{_|y~c|{yy}a|{zy~d|zyf|{z~c|{yzd|{zzg|{yxg|yf|{zh|{zh|{zh|{zh|{zi|zyi|{zyg|{zg|{zk|{yl|{zj|zj|ym|ym|zlxxy{|ykzyz|xoryxy|xpryyz|xrtz|zwwtxwz|z  =::;;{;8::8s8k4r> A>;::9;;8::8Բ8Ȩ4> [Y[Z[Z[Z=[YZZ[Z:[YZ[ZZ[Z:[YXXZ<[\Z[Z<[YPZ=[X[ZZ[Z[[Z8[X[WZZYZ<[YXWZZ>[WZYZ[Z8[XYGZ[YYZZ[Z9[TFZ[XWZ[Z4[G[Z[ZXXY[>F=<;@8^664C2 w1 B/ R."x(6d' .>r$%?X)Pp +&Ef0 $XMfiXx4 "4>=:7741/ , * (&$ #q'p) 󋏆p( 󋑈q' 󋴊s& 󊅋u$ 󊆊|"d 󋊅x!Uщu)D<Ex&Ru[`{y8ljmu_z G~{ Ы y􋊋z| twm  wg "xc 񍈎ya >;:6741/ , * (&$ #') ( ' & $ "d !UڶFD<6EպBRض`cluz k{  y tm  g "c a [Y:[Z[Y7[ZY7[YXYX4[Z[UV1[ZY[YXZ/[ZX -[Z[ZQ *[ZYZZY *[YX&[ZYZYW%[Z[Z[[Y[[X[Z[ZY[[Y^ Y[ZXY[[Z\#Z[[]\[\[X^Z$_]' []TY[Y`^ ) [Z]WjZ[[Y`^!( [Z_XTY[[Y`^'' [ZyZVZ[[Y_^0& [ZVZWZ[[Y__9$ [ZX[WZ[[Y^bE"d[ZUZX[XcoIy!U[ZYZ[\KTwD[Z_XY[NF{<[Z^WX[[ZbsXE[Z^UX[[ZTNSR[Z]SX[K=Q`[\PW[Pkl[]SX[D7u[]UX[Yz []2X[XZ{ [\[\T^U y[ZYZ[[\Q`YA t[XYX[\W_\9m  [Y[\W^^9g #[X]_>c SW[X\`D}a =:x@9Ǜe7ב=+6d'2豩Q /r4.ïH2 +m?"  (Ĩ\B #ѝb: ֓nR@ȳUE׫rL)žgINYLJ2 09A;.98&-3(.*$" #w{wsԂ v ދ x S ` ݂ b ܂b ܀ ~ | x  u pli xg !h"k#p$v%} &'(|*t +i , \- O. D/ E/ T0b1m2v3|45׀6ׁ7؀8|9w:o=<=      ݂  ܂ ܀ ~ | x u pli θg !h"k#p$v%} &'(|*t +i , \- O. D/ E/ T0b1m2v3|45׀6ׁ7؀8|9w:o=<=[ZXVVSLY[[@UUX[[2VUX[[$UVX[[OTUX[[[ 2WVX[[ aPWVX[ ;WWV[ &W[ =WW[ ݂?U[ ܂?[ ܀[Z ~ [ZYY | [ZYU x [ZR u[Z[ p[ZYZ[l[ZXUWi [YUKg ![Zh"[Z[k#[ZUZp$[v%Y} &V'(|*t +i , \- O. D/ E/ T0b1m2v3|45׀6ׁ7؀8|9w:o=<=- 8!:":#:$:%:&:':(:):*:+:, 5- ݹj+. ~?/ T!0M2ӬJ3ژl)4{05z#7ǚi 8H*9;< = *+ , - - . / 01345689 : ; ><?? *+ , - - . / 01345689 : ; ><?? *+ , - - . / 01345689 : ; ><?? 2zvh`a2xvh`a3t>tfa3bgca3feeca3ijiea3momga3rwsia3w|i`a3o~ia4V}iba4h|eba4ffgda4stuja4ola5Clca5_dba4moib`a3xla3 la`a3cmfb`aa2lnhb`aa1mzia`aa.]dma0ksgc`a/ySpd`a.σbsb`a.hcaa-rfmc`,eca,nfje +wjrg *wltg *nkn )wo| )na(yl '}"'e&|&%%$#"! &)' !"$%&'(*233ϴ33333344i444455433ӭ321.0/ν..-,, + * * ) )( '"'&&%%$#"! &)' !"$%&'(*2jox|2iNox|3Xpy|3|y{|3yzz{|3xwxz|3utux|3qoqw|3eWkw|3Rhx|4&ix{|4xiz{|4{{zz|4rqpw{|4Vav|5bv|5{|4vuy{|3mSu{|3ev|3^zvy{|2~vuy{|1vnx{|.zu|0rvny{||/ntz||.d|r{||.py|{||-r{v{|,ya||,uzxz +pvry *lvry *uwu )΁tj )u|(mv 'i"'|&&%%$#"! &)' !"$%&'(*545 }5Z5N5I5?5#6 6y6b7;7077|7r888h9L999i::;f;';<\< ==g=>a>@a`>a_>ac>ag`=ae>aec=aud=a^fd>>>==<<а;;;ľ;998866²654ݦ3212 3 ſ0 ƶ/ dž- , +*)|}>|{>|y>|z>|z{=|oz=|}yz<|utx<|kuy;|nuuw;|hvtx;|vy;||tz9|kplr9|ty{8|hl8|`rsr{6|mmnz6|}y6|xsxz5|yz4|zv{3|}o{vz2|trcy{1|}zy{1| pd~{1| yw{0| zw{/| xwy{-| xv6y{,| wuax{+| Ԁxlw{*|<{mv{)|@>>W= }=V=#<L<6;; : 9 r976u6:6654 +2 c2 / 4. ?- D,H+K*N*Q+W* b)abgtp{}nrh` adlxwXuyic!abfv_uuw v|ic#abiqxmrp yqkc`$abhjpaq{x€uqkc`'achj~Yku|{ncXkhc(a`abflioGefcdd_Tdokcb+a`bdagmoniggjmniaeda`,a`a`ceedbaabcab`1a`aba` a!^"̿ ϫ# %͜(ѿþ*.15 |{ypslulurx!|{umlWhl3x{"|zpqpu ppkw{#|{xrourv lerAw{%|{yvs}rks곏gprw{(|{xwivpkTwUkv{fvx{+|zvxuyz{{z}mztw|{+|}{z|yutuyzywuux|zz1|{z{|{z5|}|{ |=F!#\j#[%a%6+(; _*Ӓ5 C.ƪF $?2ʯ? ab<<<;;:ļ::98888666Ů5333321Ļ/ / .ſ .ķϷ -ï + *)('&|}}|{x>|s<|{xb<|{vx=|ux;|{yzx:|}{pug:|{zs":|zrtf:|zUp<9|zwzr8|{xoss8|yv8|yqt{8|}yu6|{x~6|vnqk5|{xj5|tiow3|{urt{3|ysip3|{z|i3|{wz2|{|{1|y{/|zx}o /|zptt .|{y .|zl -|{ww +|{zvw *|{z^vw)|{yhuw (|{ylw}'|{xoz&|{xq{dy>>=c==>?55555ȹ666666c767788889t999Ѭ99:::;;;==>>?|zywx5|{zxy5|zxtv5|yueq5|ytm5|zuh6|{x{56|{zxy6|zxuw6|ywsv6|yvpt6|xsm6|zwu6|zwtv7|xumt7|xvot8|yt{8|zyz8|wupu8|yvru8|yvm9|ypz9|xvux9}uie9{uc{9ws^t:tlpe:u|P:r7;mX;rp;u=t=>>?z7z7p 7N7778v8g 8a 8]8K8!9X9P9=989X:?:,:2:i ;)<<h<.< == ?RUWYZ[TZ\([PXX&[e&[i-_dW#[@N(aXZ [:\Q]YZ[,"ZRZYY[>$VRVZZ[VPUVZ[ſk J][WZ[!ȱ}3:O\]X[Z[#ӚyIQV\SZ[ZZ["ΒXF?TTUX[(wryLOZYY [,~x^NRZXWZ[0^jc87[Z[YZZ[3=X\[VIXZ[[Z3~HJORYYo6P;<<2;2:B9Q8^5h4n3r2u3u2t1q0l0b /T -? ,* 􉍖zb |d|  {} {  x'  u/ r3 Hp6 o6s6y1 󋉋7ndw(CeXmO~#l+y4|7~6"66765|5a5r6xT54m3vo0m,􋊈}j$󋊋#{` 򋇉J ry~늇}&$$## }7yvz񎌋w^*10-->@?>=<;:6/df3o 4t 6u7s8k9` :R;B==>b d   ſ3  ȺB  ̶M ϲT oүW ӮX ܴW ļPuפǺBsޟ֩x#֧)*ؼ456:666665͙55Ʉ364Ѭ3Dz.̪*¥";җw# ?=< ĶȷFQOJK Udedbba`_^VL2_f3o 4t 6u7s8k9` :R;B==>[Y[Y\aJtb [Z[Z[bOld[Y[Z[bPb WX [Z[bNP[^\j [Z[bSO [Z [Z\aTM [Z [Z]`UK [Z[Z^_WJ [YZ[Y^^XH! [Z[_YY[X^]XH![ZWUZZ[X^`]K [ZW]Z[XdXSN [Z[YT_Z[YS[\ZG@SM[Z[ZW%[ZWTV[]A8ZE[ZXWVS5[ZXURW$[\ZD [ZYXWUTvU+[YX[M4[YeqO7[Q6[\T6[ZTT^6[ZXZ\7[Z[Y7[YZY6[XP[^5[XlR>5[XlH6[XM46[\R4[WUWF3[]RGF1[Y`_UE,[ZXZ[TWTPCXYZ$[Z[YXO\VX= SRX[Z[Z[YWYkZ\ZXTW. 9>DMX`ZRVXYZ[ZXXWVQY^[\\ZXXU s76:I]]^][ZZYWXVO:&(&%$#!f5f3o 4t 6u7s8k9` :R;B==>+., - . / 0 123456789:;<?>> +., - . / 0 123456789:;<?>> +., - . / 0 123456789:;<?>> {\umb(attn(aptn'ahtjyk&a~qh%ats?CB $!" # C&' &)p* @r+ 1y -2h 0 1 2,4&?7*:90Jg=!c @>abccbb6abacfifggc4acfgdfqB|qfaceec*aceedafpwin~zuoe^rprgbdcabcddcaenmpLfosrssmnopg[xsoidbacabcbaababaabcaacabiqq|@[llmooΤ{urnheTtmrjafke`dgdadfcagi`bnojoyehpov~Zsz||xslqiafkeb2W\i`bmnjnvyzypY ><5+ ɹſεĽãꥱ꽿 >|{7|{||{yxy{4|{yy{ys,jry|z{+|z|zrmMxuilosy}stsx{{zz{ |{z|zvutytpqrwtuutxnqtxz{||{|{ |{|{|{wsskvvutvΛ~n;qruxzdpvsw|ywz|zxz|zz{|zx|{utwtlbyxttohhw@rmlkmrvtw|zwz{4bi*x|{utxuolklstp ?90g<$e(Ɯ}d9-iɲ8#ajtWsk"aiw`tu!agmw aeqwacb,uxacgd\|qabgzhoaeHsr~ab`racdbaaTm a`clpskdo!adny^Ssh"a`eknwj|$a`dmsjr% a`cfehpiv% a`cfhjl\mtx(a`_cfhknpZlr)a`aaddfnopelw+a`acbahki[o(,a`acchdaoh{/bcbbjioffhvk1eiibvjfce3jr~crz5Cpvv:{<&%$˽ɱ#˼"! ş÷ !ξ"$%% ǽ( ʿ)+ɳ,/136:<%|{wq{lc%|wp}q$|wrs#|wqq~"|xo}q!|ygvn |z&sp|{qu|{yz~jo|ymxo|{qr|}_q|{|t |{wtov{z!|}zun~q"|zvuoxjl$|zuqqpwy%|{yyxscxt% |{yxwvuqt( |{yxwuuvi)|z{ztzvn+|{||xwxt,|z{x{|t:yka|/|{{|vxtz{xpy1zxx{nwz||3wsizql^v5tpp:jpҸ<&%$v#l"f !d `^e0 d"O#D$Ձ%j'8(9)+ ~G- ŗW .r40ȑG3ۯN4ƞY,36“?":i; ++%*6)G(V'a&i %l $l #h!b  _[YXYZ \ ]_ a!d"f#h$g %f &b 'Y )M*;+ &, .++%*6)G(V'a&i %l $l #h!b  _[YXYZ \ ]_ a!d"f#h$g %f &b 'Y )M*;+ &, .++%*6)G(V'a&i %l $l #h!b  _[YXYZ \ ]_ a!d"f#h$g %f &b 'Y )M*;+ &, .kkZZ--qPasted Layer #2!? "     3%$#)q393E3Q3]qy/fo?&Xos-q b ~O$11=EHMS[cwhsyQ_K7[*GBTd Xi '****,0$11133) = = ; : 9 8754321 0 / . - ,+*)+*'& % $ # " !   !" = = ; : 9 8754321 0 / . - ,+*)+*'& % $ # " !   !" = = ; : 9 8754321 0 / . - ,+*)+*'& % $ # " !   !"#$ % & ' ( ) *+, - . / 0 123456789:;<= ?#$ % & ' ( ) *+, - . / 0 123456789:;<= ?#$ % & ' ( ) *+, - . / 0 123456789:;<= ? >={=<ά<<;;::99988 >e=oj;>R=E=s<6;;';Y: 8r9z8}x}5k~~x2z|t|/Ń}-l+}~z (Ҁ}dz &|kT%}v#}"qy jyzi}d}i|l|}  !q#na$~t&y'|(~)* + , - }. ~{/ }u0{[1^23~4}5zQ6}6y<}7Ty7z9|9k:Ā;><;<=>>Pg8emice4xLhicRmde]2jjgZokd\][/hffdi[_^][-vRfbuhf[][\['kii`lb[^^ [&jigzlc\^ [&bu`ic_^Z[%mob\^`^[#mfjd_\["cpma_\[ Fl]c\Z[jcZ`_[eiGda[,ggw^a[zdgqkb[Z[rf^hfe [sf8gecZ [dcfd^Z![\YdecZ#[Sfzd`Z$[hoc_&[lgkc^'[hfib]([eega\)[bcfa\*[ abc`\+[ dcc_\,[ hef`\-[ igia\.[ khka]/[ qinb]0[i|b]1[qWd^2[ee_3[afg`4[dfgb5[`gd6[g^6[iic7[o_mdZ7[dlb9[fb\9[Xdgd:[e`;[>[cdb<[^eb<[ne=[a\=[bZ=[\>[Z?[<57w4L1 /' ,J*T)T&X"1x#Jz!D >dG^< &!8#C$a& '(4)P*f+w, u- f. P/ 60 0 1 2 k3P4=56~6&7 8R98: :B;2<4<G==d>أހ~}~}~{~}~|n}|~zz}~|}z}z|||}W͜wt|~񋇌u`| c{\t{ &􌊃/􋊋r3~6|r}|9w< Ѽ  (/3d79= ֞jhihcgie`gg\cga`eiaajfdhihabtviijhgd]klichjf`hh\cgb`eibakfdikjSVbggjjh$ȗmacpihd^\_[[\\[\[\\[\\[[\\[[\[\`[bhho|aehigz yaca\[\[\[\[`bfk}`ddoix [\&[\[]def^fed~/[]\[agg_`qgm3[\^]]`Uhhp6[\`jq]ji9[\_]]o<[\^\ [J)LҎfmc,-`3*[/ֽd4ˑ7Ĵr:ä9=˨ =}~x;yr9>7s~}5y~3u}1P0yw. bw- x, {*~u~){('%$ێ#s"~e!u yzu] !#$##p#~#s|$y%x'|t'l}(*S*{*~*y+~,~ ,~ -*x . . /s /{} /~{} 0p 0y} 1 2p} 2}=;9׳7531ҽ0. - , ӛ*)('%$#a"! Ӯ ###"##$%&''(****+,, -خΊ . .ؚV / / 0 0 1 1 2 1=ig^;lo]b_9_a^`e:7[]]\bpbhfl5[^]fkShi3[Z`afpfhj1[Z\acgbZ0[Z^ba`]l^.[Z[]^aycmp- [^demq, [Z]bek*[^iphfw)[]hgidr([]ffd^|'[\ad4[%[\^S$[Y#[p\"[hZk![fYl| Ckq[eks[\dnv[\bMy}[_V []YU![\Z[#[\]$[_c#[^edi#[_cpfy#[\ahg|#[Z^dojo$[Z^fmr%[Z]hOl'[]jf\'[]aui([]he)*[dn*[]Xc*[]gf*[\cai+[]hgt,[^hg ,[\gi-[]m .[`cS .[\ /[]p_~ /[]fjc /[\iki 0[`g d 0[Zhmi 1[`^bp 2[dqf 2[fig@H=Ѫ;9Z+4l551 w2 )0 j)/ <-P ,*W )(('% $#4"K ? 2-+++ ,!($$V%%&T%8( (n)**w++,W,--..0@ / 0 1m 1 2B 2 ;:9876432 1 0 / -. 7- <, =+;*6),('&%$&#"!  !"#$.%8$=%?&='9(0) ,- . /0 1 2 3456789:;<.=8<=;:9876432 1 0 / -. 7- <, =+;*6),('&%$&#"!  !"#$.%8$=%?&='9(0) ,- . /0 1 2 3456789:;<.=8<=;:9876432 1 0 / -. 7- <, =+;*6),('&%$&#"!  !"#$.%8$=%?&='9(0) ,- . /0 1 2 3456789:;<.=8<=?=><@;?=><@;?=><@; @ @ @ @=/XV::XV[;8BZW\M[]6F_Y]S^[]a4!YW_V_Z^a3Q]V^Z_a12[IY_a0EXnpS_a-_\]` a--iT[W` a+WX\Q` a*L5Yr[a)US]S_`a'/\YWa&Q\V\`a%VSbY_`a$ZX9Ya"\[V[`a!G_]Z]`a Nj_]^`aOb]_`aG'c^`aH5gW_aG8X]`aEjV^a>`U^a)c[T]`aYXR[` aSUQZ`!aIRKW`"a#dOKT`#a^QFR%aPLN&a H.J`&a[\W['aLRTT(aEKBO`(a/-Q_)aUY[[*aJPPU`*a<d:yr864I31j0ݨ-, -aѩ ++ *c)'e&%Ƴ$x"! ӾûNǽrΰu6ӮYǷ3 !5"Mȟ#)$&#c&''(d[)**<>pm:KqouG8Ttpvcuv6Zyqxjxuw|4,qpznztx}||3hwnysy}|1Bt^rz|0Zpkz|-yww{ |-urp|&hulu{|%ojrz{|$tqMs|"vuou{|!\zwswz| dzvx{|f~xz{|[3y{|^Goz|\Jqwz|Xox{|P{my|6ulw{|rpiv{ |jmfs{!|^ibp{"|/e_m{#|xiYi{$|fad&|]<_{&|tvqu'|ajll(|Y`Te{(|=;hy)|nqst*|`hgm{*|< &^: h8w6 43=1 1 /6 .,+r)(1$ r#""+!2"7!= >AE9) !" ##$s%"&'(i)$)*+#$% $'()*+,- . / 0 1 2 PTWZ]^_`` a`_^]ZWSL_KTWZ]_`jLUY\^_`` a`_^\YTEd`^\YV]KV[^fCTY\_`a_^__``a`_a`^[WRbJV[^jRZ^`_^^_` a`_^^`T[_jRZ_`^^`*aA=aUW>aK>a_@a@<ż8ż4ž1¿-ٽ* ̺( ¿ͳ&¼$m!º£ "#%&'(*Y*þғ + .{ .ý /ü0ļ˥1ý2þ3ÿպ45ÿ6ÿ7ÿ8ÿ]9;<=>@@j]x<~xsmv8z{xqb~xsm{4|zxy|yqe~ytgs1|zxx{ysxqf- |zxxyoxoj* |zvzxlwogx(|yu{vjrit&|xuypskt$|{uws@tlu!|vuvaunw|wsyiwlw|wr~htM|ypp|{xe{xe |zsxq_"|zmrb#|{w[q1%|{qyn&|{xp`~'|{zinem)|zfhw*|ynYo5i*|}yvf\g +|}zzjci .|ylLgl .|}xn\jo /|}xpenu0|}xrjqx1|}ysmvo2|}ytp}z3|}zvnw4|}zukqw5|}ztgmq6|}zqajo7|}znVgr8|}zk%g:|zg,j;|{jj]=|mp>|`>|y@|>a:檄|\!6vqO2 c?0 eW,-VH*M#(O!%S#] !m c "`#%&(<)*,5-d , - 0# 2( 3- 4/3/4%789 9:;<\=>@7}7y7w6}66~ц5~z5~5z5|5~4}4x4f4333z3y3}3|3|x2Ʉ2E2}2| 2| 2| 2|q 2|s 2}w 2~{ 2}z 2~ 2 1 1v 1 1 1l 1 1 1z 1u 1ڄ 1ۂ 1f 1Ƃ 1섃 1k 1 2 1 1v 1 0 1 c/ Z/ [dXz- `VSS, a`Ro,w a_i\`+ aZYXa* 77766655555444433333332222 2 2Ъ 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 0 1 / / ʳ- , , ͺ+ * 7gh`[7aeh_Z[7mI_\[6ei][6`a][6ga][5hk^[5gh_[5dZ]\[5i][5gi^[4wgh^[4d\^\[4sS[4ef][3gh^[3ef^[3d[^\[3hH\[3gi^[3hj][3jm][2cd][2``\[2h]a\[2i;\ [2i- [2i [2jr\ [2jq\ [2in\ [2ik\ [2ik\ [2gh] [2ef] [1gh] [1kde] [1de\ [1ff] [1`fg] [1ee] [1cd\ [1pff] [1jee] [1cd\ [1ef] [1Xfg] [1ee] [1cd\ [1^de\ [1fg] [2c\ [1cd\ [1kfg] [1dd\ [0cd\ [1yef] [t/yfg] [su/cd\ [tq-cc\ [{mgj,ygh] [|{i,ldd\ [|yvx+cc\ [|tsqu*ef] [9Y9[98$887P7v776~6665|55554M4X4L44 4 4 4 3 3 3 3) 3[ 3X 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 \0 0 / . [4.+늋xp~{x}{o)}~p~s(S y&z$!w&~ |~~t}u~gQ|Mn|`}|};}|{}}||}}{|}|}~~|}~}}~}|}~}5/+)( &$!ε ߪrؽֶժU[Z:[Z[`]\^^\4[]aa][c^\aa]^Z.[`aggTb^\``W_ac^]Z*[]\lrfefiabgp^aJha])[_`TihSspee([_h`~} k[c&[_jfdx\`$[]eehSg\![\_`bmi\[\]Nccb[_kiplj\[^ohfee[_ddj^__[_fe^g[]kUa\[^tjc[\_yhd[\Rh`aa[\]Pia[]`hffe[\]Hhb[\Wi_aa[\_hdee[]`hded[\Qi`ba[\]Niabb[]`he[\^"hcdd[\Wi_``[\_3hb[]`hded[\^Ui_``[\Qh`bb[]`hded[]_hcdc[Ui^``[]_Th`a`[]ahf[\^Ti`[Th^``[]`he[]`Ficdc[\Vh_``[\^Qiaba[]ahf[\^Ph`ba[uhmokmt|1d[Fzy*6 :'J$i#I#x!@!a'   ~   2y3az33E3u44w5{5w~55N5a}6fz666}74{7q|767q8n8}7x7y~8z787{~7;77|8u8x8}8{9x8}8~8{8w8}8|8w8|8}8|8y8}8~%w%{%~%{'u%|%~&z%x%~&|9s8}2333ٻ3445555566667ֵ7777:88888888;888888:88988898889888%%%'&%'%&&;82[be_c3[eyj3[bfd3[\]Y]3[\aoe3[Zbdc4[adb_5[_i5[cmh5[^__4[Zbed5[\b{h6[cxk5[Zaed6[_aa}6[]ah7[`k7[bqj6[Zafd6[Zaed7[_a`U8[\[b7[\^g7[\amg7[Zblh7[Zbkh7[Z`dc8[^__7[Zcki7[Zafe;[7[Zafd7[Zbfe`8[\^\\8[]^\b7[Z`c\f8[_b^e9[\Yc8[^a\g8[`c]f8[\]Ye8[]^\a8[_b]f8[_b]e8[\][b8[]_[e7[Z`d`f8[]_[f8[\]Zc8[_b_e7[Z`c^f[\%[\]Zc[\^%[\^Ze[]_$[Zae`f[\_^%[_a^d[]a\'[Z][_cZ%[_b_c[Z`gZ$[Z`c_f[Z`gZ&[\Yc[`fZ%[]^\b[_aZ$[Z`d`f[\]&[_b_d9[\Z[8[_a^e2" 3I 3 2* 4T50505E56;576a6n 6:7G7]7h77<8L8P8W8f88(8-9X9\8;8M9c8U8U9c9d9f9d9e9f9f9f9d9e9e9d9e9c9d9e9d9c9e9e9c9c9d9b9d9e9d9b9b9d9c>>>?:;938&765465 2 1 0 / .,+*)('/&9%?$B#A">!6 * !"#$%&'()*+,/-: .@ -C .C /@ 091.456789:;<=>??:;938&765465 2 1 0 / .,+*)('/&9%?$B#A">!6 * !"#$%&'()*+,/-: .@ -C .C /@ 091.456789:;<=>??:;938&765465 2 1 0 / .,+*)('/&9%?$B#A">!6 * !"#$%&'()*+,/-: .@ -C .C /@ 091.456789:;<=>? `, `, `, @6BR_+aRWY[b+a FMDV`,a cX]b,a VUOZ.a LSGY^.a `^[^b.a SYW_0a Hg\_0a QYW_1a @c^`1aPXS_2a7_]_2aJWJ`3a3]\_3a2U`4a`[Z_4aRm6aZY_5aT_``5aTK_6aB[Z_6aX_`7aVR`7aP\\`7auc`8aWU`8aP]]`8a%d:aUQ`9aY`9aO]^`9aGh;aUQ;aZY`:aM]^;aQja>aN>aQ>aS>aT>aU>aV>aW>aX>aY>aZ>aY>aX>aW>ar*+ , Ű, - - . 0 ͹/ 0 Ż11g23]3i4454566777888E9999;:::;;;;;<==<<<>=>>>>>>>>>>=ô>=ï>>FVjy+|ipsu}+| [cWn{,| pw},| nmfs}-| bjZrx.| zwty}.| jrny}/| \uy0| hroy}0| Rxz1|fqjy2|Dzwz2|_o_z3|>wvy3|Am{4|zuty4|h{5|try5|ly{{5|kaz6|Uutz6|qy{7|mhz7|fuvz7|~{8|pm{8|fvw{8|5{9|mg{9|sq{9|gwx{9|_;|mh;|rq{:|cwx{:|g{;|lh<|rq{;|uu{;|yy<|{{<|{=|kg=|on=|r=|v=|y}|y>|>|d>|h>|j>|l>|m>|n>|p>|q>|r=|}s>|r>|p>|o>|R,",-<-. {/ ?/ . o1 *1 {2 2 3 3 4445}56"667>778R899199::,::;;+;;:::9#7j675~7 5n45 3M422<J3"b3){39393939393939|39a)bab\T(Uab]Y)\a^_)bab[[b;a_[];abY[b:ab^X\b;a\^b:ab[_b;a`Y]b:abX^b:ab]S\b;aP]b;aZ`babaU^abcehifhacaZ`aciks}~~~yqxlbeaQ^aeiuyhrpmgaW_a`hU ttea^`a`iTxu` abS`a`jQtZ a[_a`iMtf aP`afClhaW^a`Tcga_aem]aUahe\a]abkKa`Rbada`Y[aena_\bafna`Xbabhda]Zabifaa_Vbaila`ZWbajga^Zcabkd_\babil\Tbai\Tbabkb_Z`abjdb^Tbaha`]ajg)Ĺ(ú)(ķ;:ij:ü;:ø;:IJ:û:ã:ö!ëõî  è  þ½ۿ۹۸۾ü|*}|}vl(m|}wr)v|{xy)}|}tu};|zuw}:|}ru~:|}xqv};|uy}:|}uy}<|qw}:|}ry}:|}wkv};|hw}:|}s{}|{|{|mx}|{xtutssvvx{|t{|{yurx||{}|pszz|ix|x|xuclzrx|}oz|}v] qmy|x{|}xVpjtz |}kz|}wPix |uz|xOkx |g{|{S|fw|ox|ngy|zy|ypr|l|{wgc|w|{v@|{j}|zkƀ|{rt|yjߏ|zv}|yh|{q}|{we|xr|{vd||yo~|w`|{tp}|vd|ys~|{ufzv}|w`vl}|vbvl}|{uhyt{|{uf}xl}|vb|{w|vc-G.@/ 90 51 22 13 04 4?4D67W68k8q98:9;:;=<?>ab>aU^ba^Vb=a`^=a`Sb=a^[=a`Q0ae a]W/a[ a]_.adb aU.ac a[]-a]c abS`,a`caY,a`cabZ_+a_cabX`*a`caW]+acacV_)a`cabU[`(a`cacU^)acabT^(a`ca`\`(a_cabT_*ada`*a=<;Ů;ú;ļ<ķ;ø<Ŷ;÷;Ļ=ĺ;ij;Ŷ;ij;ļ<;û=;ü>====0 =û. ê. - ħ,+ĵ+ı*ï*Ů(ì(Ŭ'Ī'(ë()|}>|lx}<|vz<|~ot};|}wqx};|}xw{<|}uu};|}vtz<|~tr};|}upy};|}xt=|}wr};|}rfy};|~tg};|}rau};|}xn}=|{w<|}wl}=|zs};|}xi>|yn}=|zx=|zj}=|xu=|{h0|z |vp/|q |wy.|t{ |l.|q{ |tw-|j{ |}j{,|iz|rs,|i{|}sz+|j{|}q{*|j{|pw*|i{|~oz)|j{|}nu{(|jz|~mx}'|i{|}lx}'|i{|zw{(|jz|}my)|i{|{z*|8X8P7I6D5 @2 =1 ;2 0 G1 1 Y0^./s.z,,++***(((''&%#$"! " !q"O*:IIIIIIIIa`WK*i a^`[\)샃 aZRUv(Ņ a_]_h(x a]ZY['w a_ZJ'ք a^\[\&߃ a]MVx%f a^[]g% a^NW$ a^[\c$l a`3W# a^Z[d# a`zV" a^YZq" afS! a^VX" a`! a^TV! a_]]m! a`jF a^VX a`_^Zv a`O a_XZ a`_]Yh a`IS脄 a`Z[˅ a`_^Vd atAσ a_?K a^OQ܃ a_YXZg a`q@ a`@J󅄋 a_OQn a_XXw a`l9 a`EMy a`WX a`\\ a^ a` ac{ aNR a`UV a`YY~ a`\\w a_󆆋 a` a`g a䆅 abІ a>d aGNф aFK a>Et aEH{ aHJ a`IK a`IJo aGI aCG aaU>aR>aR>aN>a?a`=a\=aW`>>>>>=<<<<|k>|j>|h>|d>|~|v=|po{<|ji{<|ec{<|a]=|^L=|a{<|Htv{;|oo{;|jh{;|da{;|_O<|Vx<|Myy;|sr{:|lg{:|q{;|>wv{9|fbz9|eV{9|g:|Uvv{8|!qo{8|o9|nz{8|wv{7|p8|sxx{6|tp{6|8|ixx{5|sm{5|uxy{4|usz4|uy5|jzy{3|{zz{2|qrpx2| uxyz1| Rlgt}0| oy{{0| Gg]t0| fuux/| &gy/| Tols}-| Vly{-| ;mmes},| ;^`}rv,| ;^go]w}+| ;^[sks}*| ;^[cbmu})| ;^[[imfsz)| ;^[qtw}(| ;^[Zu}'| ;^[cfw'| ;^[hmaq{&| ;^[qtqw&|c38J3934B4 5D5R6T6O46Wu7S>68K79:7:8f:9;::7:::l9999V888R777,66555F44 ^3{3 M2 n2 D1 d1 !0 Y0d/=.Q.-A-_*+7+[*{) (5's'&a_Uablaa^Qbaika`Xbahka_Sabkba`[babkca`Ubaioa]SabiiaTbabkba\M`aia`Nbahla]QbabkdabRP`abkga_G`bailabWT_ajg aK_babk` a[W`babjc acOU_aig a^J\bajd ab[\`abkb acU]baih a^aij abX[abkaabTZb abjcabY\ahkaNVb abjjabX[b ablaabWZb ahkaWXb ahkab[\b ablaaYbabjdab]]b aipa_\ ]^`u oa `_[ VU]ab:ababaþæð  ö š  ĸ ū  ı é ô İ ı  ÷ ó û   Ǿ  <|ym|{th|wh}|v`|zq}|v`|zj|{th|{u}|{uf|zm}|vg|wj|vd|{l~|{th|vc{|vb|{c}|w`|wh}|{uf|}igz|ud|y\{}|wa|}pkz|ud |`z}|{tg |}to{}|{uf |~fmz|vc |y`v}|ue |}uv{|{tg |~mw}|vb |yx{|va |}pu|{tg|}lt} |{uf|}sv|v`|{dn~ |ua|}pt} |{sg|}qs} |u`|oq} |v`|}uu} |{sg|rq}|{ue|}xw} |ve|yv wvs |{z {x ts tsy|{<|{|=-><=<<;;::99887565 v 3 3 2 / . - . I-RpbcaW`+adaQ`+a`ca[`,acaS`-ada^-a_c abX.a`d a`S_/ac abX0ad a`U`0ad aT1ac aS_1aca_PWb0a`cab_K2a`da_T[b1abdab^Ob3adab`X]4ada_XUb4acab]Tb5aca`]^6adab]Xb6ada_`6abda`ZYb6abdaabUM`7a`dbaaYWb7abdabTRb8abcab\[b9adb[[b:afXXa_aba*å+÷+, ü- ð- . ò/ / /§0þ0þ2þ1ý2ÿ23ú44ú566Ŭ67Ū8ø8Ķ9:ź:dz;Ǿ;=>i{|qz*|i{|hz+|jz|u{+|i{|k{,|i{ |}x-|iz |}q{-|iz |{jz.|i{ |}r/|iz |{m{/|iz |l0|iz|}|jy0|iz|ygo}0|jz|}y`2|iz|ylt}1|hy|}xf~2|hz|}zqw3|hz|zpm~3|hz|}wl}4|i{|zwy5|iz|}wp}5|iz|{z{6|iz|{ts}6|iz||~ncz7|hy|rp}7|hz|~li~8|hz|}vu}8|hz}uu}9|h{pq;|h{wx;|iou};|hvz<|tz=|{|{>|{|HIP] [ a g!W!d !_#"N$P#$C&%0'*&')* *+,,--. /0 0 1 1 21 32345RH=xa@ a* ad a`a]vka`[[_ka`XXa_ka`STmaa_ka`NPa_ka`ILa_ka;Ha_kaz<ea_ka`]\y߇a_ka`XXՅa_ka_RSca_ka_LǑ a_ka`?J a_aj2rai:{acaj?af6qah;ac-ab} aab a a auababac1jadSadXaiUyajUtaeVڌadVڌacUcacWƌajWajVra`^acadxv]adtradusadvtadusadtradvtadusadsqadsradusadsqadqpadtradtsadrqadrqadtradqp a-  L |  ԀьƊԐ̍ώǐĆ ċ   ēēŝȫȮѫӫʮȮǫƯԯӭ |Vzf [|I}`Z [|a\Z [|zeb\Z[|{wwncwb\Z[|{uua}wb\Z[|{qra|}wb\Z[|{klbe||}wb\Z[|{ega|}wb\Z[|{^b_|}wb\Z[|M]d|}wb\Z[[|SYb|}wb\Z[|{xw`|}wb\Z|zqqb|}wb\|zikV_|}wb|zbeZ |}w|{Q`[ |}|Gg[|Rs[|A[|U[|Pg[|S[|H[|~#u[ |{[|~,[ |[ |[ |j[|}+[|}7[|~R`[|i[|l[|lo[|mh[|m[|m[|mX[|n[|n[|nh[ |pqZ|{vb|{npy~|zqrrY|zoq|znp|zpqrj|zqrul|zpqv|zoqv|zrsuo|zqrcZ|zpq|zrs|zrslG|zqrv|zqrv|zrst]|zrstX|zqrv|zrsu |      Om#os @>=[\Z=[b\Z<[wb\Z;[}wb\Z:[|}wb\Z9[|}wb\Z8[|}wb\Z7[|}wb\Z6[|}wb\Z5[|}wb\Z4[|}wb\Z3[|}wb\Z2[|}wb\Z1[ |}wb\Z0[ |}wb\Z/[ |}wb\Z.[ |}wb\Z-[ |}wb\Z,[|}wb\Z+[|}wb\Z*[|}wb\Z)[|}wb\Z([|}wb\Z'[|}wb\Z&[|}wb\Z%[|}wb\Z$[|}wb\Z#[|}wb\Z"[|}wb\Z![|}wb\Z [|}wb\Z[|}wb\Z[|}wb\Z[|}wb\Z[|}wb\Z[|}wb\Z[ |}wb\Z[!|}wb\Z["|}wb\Z[#|}wb\Z[$|}wb\Z[%|}wb\Z[&|}wb\Z['|}wb\Z[(|}wb\Z[)|}wb\Z[*|}wb\Z[+|}wb\Z[,|}wb\Z [-|}wb\Z [z}|\|{~pzye~{}xx}~{ va} v} ~|x |}!~!z tJx"|}"~|#s"{#$tq$~}W%|l%xn%|]&|b&{n&{Ćh'z|R&{m(|j)~\)^*l*|q k+ @*n i,|x m-Ղ `, ]. h/ ^0 a1݈ a2 a2 a3fa4a3a4a6a7a6a6a5a6a6a7a7a6a6    !!!!̺""###$$%%%&&&''())** ++ , - - . / 0 1 2 2 345467667667766[Xe_ff\[^b]f5jk\[^c]gr[\]Yenc\[\^\cYff\[^c[gef\[^a_egi[\Ydm[]`[fmac\[^c^fff\[\][d fh[\^\`pP][\a_f ]ac\[]f hi\[Zc gi^[Vf!i`_\[Sf!zaa\[Xd o][Yb"hEa\[Uf"wij^[Tf#fg^[Z]"h_\[Xd#ef^[Rg$o.\[Xg$qhi_[Zc%gi_[Wq%]`b][Vp%gj_[Zf&hT\[Yj&nfk][Up&jb^[Xm'`hk`Z[Za&ke_Z[Wo(nhja[Xm)g_][Ze)kb`\[Yh*Z^c_Z [Wo*iqb\ [Xn+n`b^Z [ZW*zt;d^Z [Xm,qjme] [Wo-deb [Yi,ndfb\ [Y].f_X\ [bT/^\ [~0`[Z [|1R [|2Z [|2Z[|3Y[|4X[|3][|4[|6[|5[|6[|6[|5[|6[|6[|7[|7[|6[|6!! !""""-####F$$$$F%%% &&&f%'((v(]))A**)++I,|,?,-! ,I. /2 0J 0 1P 1 0 1 455r6 7 88888888888888 >>=<;:98765 4 @>=<;:98765 4 []>[>[=[<[k;[d:[b9[b8[b7[b6[c5[d4[ @> =<;.:A9P8K5G4D3B2 @1 ?0 <1 9-~zvtQs3s}~}zrGofs[}yڈx "f͚nvuԇ㏗瀌 []hhmn[Z"[na^^\[]jh_ikg~s<r'`c\[_`im_aec_]^_^]^]\]^ba]^a\ [\e_afd_]^__^]^]\]^bb]$[\[\[3"=M땤" wɉYga$r~t]ba'~p|wxa*|e}pv a+|w{ a-nz} a0u}na2y~pa4|}a6{txaa8f=r:~<|>> @ Ϫ$'*ʸ˾ +ʊ .Ͽ 02468ֶґ:<>> @ [^ab4^bg]aWy|$[]ab\beB{|'[]bd^abd|*[^bh`acc |-[_bmadc |-[\[[ahacb |0[\`e`cbj|2[\[^e_cbf|4[\^d_cb|6[\]d_cb||8[\]]g`ge:[\]`Xb<[]\c>[^ [!L$% ( +w.' 0=3U5j7k9';=n> agmiagmiafliafkkafhqafgwagabhgabhfahe ahd ~ra`gU yuva`O pgaX duadJ ry}aeN }mafRcua`eU|z~a`cP}oabXiafYwoabib qai ݑjrags sraX_vxgabamwxabhsxxabovva`rcrtahmracv\lbaf~bamabyafpya`xmupaha~auyuayloawayaabaabbaaba`bba튈mpnloommpokppm~$,./-0-&%&%&&}&'r'+(    ؿ ͱ  ӳ p ޹ mװּ7õ0>־üj׳Kb["Ҽƾÿӵ.,./-0-/$ώ&շg%א4&تM&׾t#&׵'زͩ'*(|xqp|xso|xsq|xst|xsx|xo{|xk|xh|xd|x` |y] co|yM \bbe|a \d^``t|{q \]_V`fd|a []\c_cb|_ [\]ba_k|}{][_`d_fc|{_[Z]]`Xcb|}^[]aha_|{p[_a^^]r|yc[Z]_\Xj|xc [\]^[mh|wc [\&Zdg|wg [\o]bj|u[\gbaw|{q[\ebd|{qY[]ecd|{rZL[\fcc|vSDB[\gcc|xfJA[\sde|{rRC[Gen{|zlC[\Ztsz|xa[\]\a|{o[]`_a|y[\`cac|[]cibc|[^_]\|[]=^`|[\_e_^|[]^a]`|[\]a|[\_\\||{||{{||{{|{{|[\]\trtussuursvsrt[\[_`_`_%[Z[[ZZ[[Z&[Z/[].[]ZY/[Y-[ZXY0[Z-[Z0[Z.[Z[\&[ZY[\]%[ZXY[]^'[YV[\^^&[ZPU[\]^^'[ZYV[[\[;>([JT[[WU([ZUTV*[Z\)[VW},.x-z+M*m(y'=%o$ $ # "  5YF& V o>{jLh!\ !m "n "6 #s #r $$j %%U&& &''((; ^y`cc%a ^|\dib$a ^ifib#a ^ gic"a ^ gic!a ^ ghc a ^ ghca ^ gca ^fghca ^hghda ^dghca ^ogfhca ^kgagca ^ig\ecba ^hf^cdca ^hfccfe`a ^infogda ^}hgLfbc`a ^heheea ^jfeVgda ^hgfhada ^igggdb a ^s[dfibd a ^igchbc a ^!tjgWfcba ^#fefjccba ^%gheicbba ^&khheiebbaa ^(lhhejebb ^*rhhfkf ^,thhf ^/h ^0 ^0 ^0 ^0 ^0 ^0 ^0 ^0 ^0 ^0 ^0 ^0 ^0 ^0 ^. ^- ^+| ^* ^) ^& ^" ^  ^ ] f  1 1 1 1 1 1 % ɪ$ Һ# " !     ּ ü  ƴ ˳ Ȼ  ½  ˿ ½ķ ¿ µ λþ !ɾ # % & ( * , / 0 0 0 0 0 0 0 0 0 0 0 0 0 0 / . , + ) & "      1 1 1 1 1 1 ;^[y%| ;^[`bc8y$| ;^[]edXx#| ;^ [hgbx"| ;^ [jgx!| ;^ [lmlx | ;^ [oqqy| ;^ [stty| ;^[usqx| ;^[spnw| ;^[^qliv}| ;^[mkfv}| ;^[kiqcv| ;^[ifbw| ;^[gbfwz}| ;^[eVrtv}| ;^[dteot}| ;^[_snjxz}| ;^[jevawv}| ;^[drqt| ;^[^snwfyy| ;^[iglvu{} | ;^[bNgv`zw{ | ;^[`hhznxz | ;^![azoww{| ;^#[fUt`xy{| ;^%[khvkxzz| ;^&[csnvhv{z|| ;^([cqltcs{z ;^*[anjr]q ;^,[amhp ;^/[k ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^0[ ;^.[\R ;^-[\G ;^+[\Q3w ;^*[\E ;^([\Q>x ;^%[\QND ;^"[QZSJ ;^ [TRX} ;^[ZVVXXW{ 9\[ZXW_yvo{ JkXZXdpnnog HeefhegefP 1 1 1 1 1 1%$)#G"e!  z!\"A#)$%&f'J)&w* d,Y-1z/n 0.y 2d 4z564Z6J86:$@acb=algcab:ahfdhgb9aj`ff\gf7aodffage4add\ggd0aedbhhd`ggd,a fgc`hhdbfgfc%abe xegd_hhfcadeffecabeffedabf xQegfb`thfcacdefefedcbbehhgk`be y cfggecamihgfghi_beggfcHszceefgfgfeed\}567|7Ԍ8⃇;<>>A==˷:ٹҷ7ǹ5߸1ϸŶ- ?˷Ĺ( Єҷ w¿ ͖ɺ}56778;<>>@|{=|Qnz|{:|goeq{7|[eUjqgt7|[feithv4|[jiPiqy0|[jvgq~mjqx,| []Kirwfp|qlqvz%|{w [\!gpvdnv{{pmotx{|ytpmny{{ [\ Fhovz~ckrw{|qonmnopqstvvutsqpnox|yrme@zs[\$ ץchmsw{|W_cgjknpruusqpmkhebZG|ysnidEt[+dfghjlmoqsuvtrqomkihfe\[45[Y=6[XG5[[\W2Q7[\Qb8FQd;^>@=:28S5{1 m0  j- g)1vϊ< @Zsǰ}cL& "  "$&&%$!   ac>af>aijd|gz=|znx<|qox<|pjy<|mYz<|ply<|okx<|nbz<|n]z<|olx{;|pkx<|m{<|ohy<|plw{;|k=y<|gy<|nex{;|oix{;|n{<|nWz<|okw{;|n@z<|n{<|ncx{;|n`x{;|m{<|nz<| @7=k<r<.<<7<8<<<<<4<<#<<< <<<%<< <=<<<<<<<adpnadrqadrqadqoadonadqpadqpadonadonadqpadnmadnmadonadonadnmadnmadnmaclkaclkacmlacmlacmlacmlacmkuacljvacjjackjhackimaciiacjijackgwacjgwackjaciewaciewabhdwabebwabhewabhdwabhewabhewabf^xabe`xabeaxabe^tabe[vabf`xabfaxabdVxabeZxabeaxacTwacFvac[xacXxac=yacByabeTx ay ay a9y ay ay ay aĿýľýøÿþõöĽ      y |{stf@|{st|{rs|{st^I|{stt|zrsu|zrsu|{tur]|{st{|{st|{tuuo|{tutf|{stu|{tut|{tutj|{tutX|{tuu|{vwu|{wwtD|{uvv|{uvu|{vvt[|{uvp\|{uwl|{vwm|{wwl@|{ww\|{wx^|{xxiM|{wxj|{wym|{xymc|{wwxl|{xzmu|{xzm|{y{mt|z|mb|{xzm|{y{m|{xzmh|{xzmW|z~m|z}m|z|nA|z}g|zj|{y|m<|{y|m|zm|zm|z|mC|{l|{j|{mP|{m|{m|{mc|zmm |mp |m |mz |m] |m |m | .a_k /a_k 0a_k 1a_k2a_k3a_k4a_k5a_k6a_k7a_k8a_k9a_k:a_k;a_ka_ a. / 0 123456789:;<=> .|}wb\Z [/|}wb\Z [0|}wb\Z [1|}wb\Z[2|}wb\Z[3|}wb\Z[4|}wb\Z[5|}wb\Z[6|}wb\Z[7|}wb\Z[8|}wb\Z[[9|}wb\Z[:|}wb\Z;|}wb\<|}wb=|}w>|} |a5a6a6a5a6a6a6a7a7a6a7a7a6a7a7a7ka6_ka6a_ka5a_ka6a_ka6a_ka5a_ka6a_a6a6a5a6a6a5a6a6a6a7a7a6a7a7a6a6a7a7a6a6a5a6a6a5a6a6a6a5a6a6a5a6a6a7a7a7a6a7a7a6a67667666776776777667667666766766677677667766766766676676677767766[|5[|6[|6[|5[|6[|6[|6[|7[|7[|6[|7[|7[|6Z[|7\Z[|7b\Z[|7wb\Z[|6}wb\Z[[|6|}wb\Z[|5|}wb\Z|6|}wb\|6|}wb|5|}w|6|}|6|6|5|6|6|5|6|6|6|7|7|6|7|7|6|6|7|5|6|6|5|6|6|5|6|6|6|5|6|6|5|6|6|7|7|7|6|7|7|6|68888888888888888888888888888888888888888888888888888888888888888 3 1 0.ʍ-,*)ˋ&$k#~#"Ä"{! ݆  !^!  ###|"!!"$$$$$$$#|######K#$$$$$$$$"""z"##! !!"" 3 2 0/.-*)($##!"!    ǎ!  !##"!!""$$$$$$###""###$$$$$$$$""!"##!  !""h3[ ~]1[ bZ/[u_.[]-[^,[tV*[)[ZZ&[YY$[\Z#[UQ#[ZY"[VU"[J^Z![XX![VVZ [VUZ [UR![R;<;<;<;<;<;<<:88777²6664443332о2 3 3 1 1 2 0 0 1 / / 0 >[ZX;[X\ZY=[W;[YXY<[\[Y;[Z<[VSU<[ZDMZ9[ZYL8[VS8[V_7[Z]W7[VWP7[OU6[ZVL6[VU%6[E85[WU5[Q5[54[XVe3[ZQR4[@3[YW]2[ZTT 3[QT 3[Z] 2[Z 2[OS 2[Ze 1[Z 1[RU 1[\G 0[Z /[ZUV 0[^N ;3;$:: :99V9 8 7`8 76l6 65u5 44`4 4 3o 3 3 2 2 2 1 1 1 1 0$ 0 0 /0 /950\,s)v}y%| !w̅1}2557:<=;;7۾}6t5p4r~0u~(z ג{~~}!vӎ ވ}}}wڍʢz~ ~uԵx yח) <92.͉,̲)͵Ѽ%Կ  Ӹ11257:<=;;77651) # ) [ZWZ9[ZWYZUZZ5[YWZYT^ZVZ0[ZWV[YT]YV6RZ,[ZXXZUJZYUqQ[Z)[ZUKZUPWN[Z%[YZ[XTZVSYP [Z![YZ[XL[YW^U[Z[Z[[Xl[XW_U[Z[[W^[XUV2[Z[ZW\[XQW5[ZW\ZXuU5[ZY]ZW_T5[ZY\ZZiV7[\ZYbV:[W[Z<[Z=[Z;[Z;[Z[HMS\6[Zҫ5\6[ZL]5[ZH]4[Z:A\0[UR =W8\'[\R?X,\ g-786Y![\P_ R46XAR\[]J6:^(;9J] [\B7UHʟ >@&p) <;-(*741/2=:5-I; ;*WDA%g[F!tbKꄀeTፋeWٙ_"YۨS&Y մT*X?.U2Q3L4F5?8892:,;(<&=%>'@(*v)*+**,t+w{+,-,wz,-/.y-../////}/.-,썅+'&%$#" !    }w~|z|!}\z6}Z(*))+**,+غ++-,ؼ,,-.--../////L/.-,+'&$##" !    ؾ!6ُ)[Ya*[bI*[X*[ZY^+[S*[ZTU*[ZVVz,[dE+[ZMO+[ZTT,[Y-[\-[NO,[ZTT-[X/[.[NM.[TS.[WV.[YX/[X/[S/[W/[Y[Z/[Z.[ZW-[HYUW,[eVUW+[\VUW[Z[YVTX[Z[YVTXZ'[XVTXZ&[XVTXZ%[WVTXZ$[WVTXZ#[WVTXZ"[ WVTX"[ WVTX [Z XVTW[X XVTW[T YVUW[PZVUW[M]VUWwVUW[YGVVW[WQWVW[SPVWVW[NP!QVWVW[9N6QVVUW[Z8)))J***i+++A+,,,F,---I-....../*/.--,2+7<A(D'G&H%I$G# E" B > 9 4{/$)%!Q " / B. :- <, :+4,)+*)('&%$#"!   !("3#8$:%8&3')()*+,0 / 0 1 2 34786789$:0;6<9=? / B. :- <, :+4,)+*)('&%$#"!   !("3#8$:%8&3')()*+,0 / 0 1 2 34786789$:0;6<9=? / B. :- <, :+4,)+*)('&%$#"!   !("3#8$:%8&3')()*+,0 / 0 1 2 34786789$:0;6<9=?>>>>@?=<7;2<(;:9876543 2>>>>@?=<7;2<(;:9876543 2>>>>@?=<7;2<(;:9876543 2wjc;aweb;axbc@ Ѿ  !Ȍ   !!!""###$$%&̩&'')()+*+,,- . 0 1 0 1234556789:;=>@Z [X [WP []Z [Z[X[] [vZ[Y[F`[^Z[YZ[S\@ w :d   ~ !!Z "}###=$x$f%z%_%z&G&v'/'h()N)*'*+ ,,-/.m 0 1 2 2# 3k 455 6789!8$9%:%;$<!=?@/ . / - -.s,,,v,} - / 0 1 234~5w657w{89:;==>A==<ԭ<;д:774/[Zd .[ZXX /[pU .[Z\ -[ZWX.[iF,[ZY,[ZUV,[ZK\-[XeP] .[WoS^ /[XUa 0[XUd 1[XUk 2[XS3[W`P4[UeM5[Ug:6[T`7[TaD8[TeO9[VgRx:[VfSj;[WpTd<[WtT=[W>[X[YZZ<[YHX<[\V\X;[fBZZ:[`ZZ8[aX\X7[lTX]6[/.T..-H--,I,,;+=,D-I .G 1D 2A 3* 4'5&6&7(8/93::;?=?~>:k9(: 97w64?=<;:98715@1K/S-Y ~+\ ~)] ~'\ ~'Y ~%T ~#L !A2    S {|_{   +<H~Q~W ~ ~!~ ~~ !P"{#z?=<;:98715@1K/S-Y +\ )] '\ 'Y %T #L !A2   m Ϳ   +<HQW  !  b"Ϳ#ݼ?=<;:9871`5@T_1KpS^/SXiQ^-Y [XcQ]+\ [YdQ])] [YdQ]'\ [YdQ]'Y [YdQ^%T [YdQ^#L [XeS_!A[XtU`2[XUd [XUj [XUu [XU [W;R [WD[VX[TXC[UN [UiP [VfQp [VeQh+[WdQc<[WdQaH[XdQ_Q[XdQ_W [XdQU [YdRT![YdRT dRT[YdRT[YdRT[XdRT[XdST [XkUV![W3VU"[XVO#[X]NB>A=B<C;D:C9B8@7?6D5H4 F3 C2 A1 ?0 7/*.'-),++/*2)6(:'=&@%C$D#E"D!C A>;=G"DDz< Nr V E{ [ ^ |U_  ] ~lZ gnT |wVsbL {x|eA'2()*+]D` N V o [ ^  Ɔ_ ] Z ơT ϑ̳њL ƞA'2()*+]DOVVUW[X$ NJXVUW[ZTT V +TVUW[NR [ UVUW [ZVVS^ VTX [ZPR5_ VTWZ [ZYXW] VUSWZ[ZXX\wEZ USAXYXYZTXdGT PMV:RUVWUIXUW>L PNTVWWUPR@A'2()*+]&*/#4 8 8= W ;W"hR"Xr#- ] 1 0 /.-,+*'-&4%7$6#1$(#"!  !"#$'(&'()**+2 ,5 -5 .0 /' 0 1 2345789: ;<= 1 0 /.-,+*'-&4%7$6#1$(#"!  !"#$'(&'()**+2 ,5 -5 .0 /' 0 1 2345789: ;<= 1 0 /.-,+*'-&4%7$6#1$(#"!  !"#$'(&'()**+2 ,5 -5 .0 /' 0 1 2345789: ;<=uzkb8a\ab9akmh`7av|k`7awj`7awajf`6ajkg`6as|i`6a{fb6atnrk`5av}n`5avf`5azmmi`4aym`4ayalh4asvo_3asxnd3atool`2awp3awice2a}t2a vRmf1a w|ub0a \jpl0a lmk0a vcV0a vy/a mur.a i|xc-apv-a xqb+auve+avgb*apie*aifjc)aPcnc(a6od'a~we'askgb&a[fb%a;hb$a|ib#az}jb"ay{kb!axzlb`atxnbau|nba}r{oa~upaxpaypaypa/toa^klaZhiican>lsdashlea&RxPgfc`a/efmlc`a3fktspd` a3sr`kjd` a0!yyrlca` a'"|pgdfd`a"ejklf``a9;888777Ϫ7666544333322 λ1 0 0 0 þ0 / . -- +++*)(Դ'''թ&ݳ%h#"! gȵ&κ/3 3 0!Μ '"x"plw9|;|wvy8|ojv8|ogw8|m|wy7|wwy7|qkx7|ky7|furw6|oju6|nz6|ux5|m_u}4|i|vy4|rpt}3|rnuz3|uttv3|nes3|vx{z2|iNp2| huy1| nkp{0| wsv0| uvv0| {0| eZn/| ppr.| ^jn{-|vbp-| nes{+|poz+|pay+|{txz*|}ywz)|ֈ{t{(|bt{'|ungz'|svx'|z&|vUx%|rfx{#|piw{"|pk0v{!|ql\v{ |wmcu{|\oit|grlt|cqht|nas|{mUs|nRt|q\t|7vbu|yVxw{|uvrz|qwhuz|&7nyy{|/Ozzuv{|3}wqrt{} |3tr}fwwz |0!zkmqv{ |'"rgsyzy{|"}azwwvy}|19n9g9)88d8a87e7<6 6`405 u5:4,44 3 k3 2 L2 #1 1 0 0 / ".--X,+}++#*/)R('#%L$W#b"k!s {! %( q!Y"D#6$*%'h( )*"+(,)H.T 09p 2: 31a|a`>a`=a`g=a`m=a`j>==<<<<;;:Ǿ:998͸8876³6533332 1 /} / . .ů ,Ƌ +*)(''&%̻$#"!ıδ Ż!#$&'?|}>|y=|}u=|}w=|yv<|}uo<|ys\<|wuv;|}wtu;|ywy;|}rkn:|zvX:|qlo9|{rhn9|uvsx8|xoG8|ut8|t~7|lhm6|sm{6|nk_5|qOy3|{tko3|ypr3|{&m3|zu{2|y{~ 1|u} /|zue /|zjov .|{x{ .|x_u ,|{wgr +|{wiq*|{vEkp )|{u\mp(|{ugnq'|{ukp&|{tkra&|shpY%|sho$|rfn#|rgnz"|rio!|qjoB|zrkuX|sufsN|qpp|uarh|zl|{vt~~Y |}ztrmy!|zvuitps#|zup_o$|ywsmq&|yxwr{h\'>z>?=&=\=1<;;: : :T9 98 76255S433z 3 0! /0 0W / -/ ,U+]*f)p*| )('(&$%$$#s "h!Q 5 [, ""(#8 $p&v> '@)}-+a`hnl:aix:aex:`iolu:`gkj;ahu:bdUz;luq ;lw;mJw ::Ĕ::;:Ľ;;;Ǹ<<<===> |xuv:|x_m:|zm:|wtvi:|yvw:|xdp:|zj;vpr;vho;vn c;;s<\<b<<`=2==U>=> ?񻂊42 ޔ0 W/ - +wz)|'x%w" f⏃us| ~$'m *w/y}}5:9876543 2 1 0 / .4 +A *K)R (W 'Y &X 'V &Q %J $?#0"!  #$"#/$>%H&P'U (W )W * U +42 0 / - +ֽ)'Լ%ӹ" ά۸ $( 'ذ */¿5:9876543 2 1 0 / .4 +A *K)R (W 'Y &X 'V &Q %J $?#0"!  #$"#/$>%H&P'U (W )W * U +OZW4[kW\3[ hYW1[ c\0[ GYfT.[ qX`WZ+[BYO]WZ)[hRYP[WY'[aTXMXYY%[^SXMXZYZ"[^UX^TZZX [n0VZcW[WZ[gTW\KY[WY[:RY[QX[YZ[ SXZRY[ZZ[$YUYZXZ[Z['YRXZZHWZ[[ZXYZ [*[TWZ[ZoMTY[Z[/_NQUY[[_QUWYZ[[5PUVXYZ[[:9876543 2 1 0 / .4 +A *K)R (W 'Y &X 'V &Q %J $?#0"!  #$"#/$>%H&P'U (W )W * U + p3 1 c0/-6+F)N'S%%9w#?O 7NRY"'[`%ap(Q~, O 21p8Py @$%%'<&&&&&&%t,%$#m |􋊋  񊇆{" ug )$%%2'a&&&&&&%ֶI%$#פ   ˿" ʱ )$[WXT%[tZ%[Y'[$&[UVf&[Y\&[Z\&[Y\&[WX_&[W%[ZJ%[Z$[YZY#[Z@P![Y[eUU[Z[ZRZPT[Z[YVZZUV[Z[ZXUkZ[YWTT [ZXXVUP^[[ZYWTT" [ZXXWVUTOA )#$$$%%!%&%%E%%$8$l"c Y͐p'Ɨ}Dҳ:% qI l @ @ @ @ #fk\}ogg``a $~|w&pcgd``a %zxwmqhfea &ctornd 'rroI(^yv)* )('& # $ͯ % & 'ɹ()* )('& #zv~kuyy| $uhogs{y{| %rmnvasxyz| &qt2rtz '{qsg(Gmn)* )('&2%x7+j9;>(a`>aea``;anbee;alCrrddbb7aw}prgeddb1abdcyxygwqfabccb'abccbahrry{tp`nooeacbbabaemlnUrqztmohytplecabab abaabafoou}~nlwz ֔y{usoc.{yulqiagkc`ffacfdafi`comiswylnsxuwxxwtlp}yy{y~|}>omirvwxxY>;Ƕ92)V ñ;ɧе|z>|t|zz;|vrr{8|ljssZhyzz{2|{z{onmxnry|{)|{|xqqplps}&uuty|{|{|zvvursidfvuxmqsvz{|yttpjiVuvdij ։njpqt{\llpvtw|yw{|yy|{z{|zx|{tuxqnlYvtqnxpnqvt_jhfmmjmnmijjtuxronK;8@3i+ʴv ͻ v2~+1@fA)  %:&  a`ghludo[)a`aagdq~r~*a`aebaqkwwz-a`afemdlu/abeebr|Lcl'1acegnuMovx>3cky{orw7-ev8y ɿ) ѡ*-/Ϲȫ1˺378 |yxvpzty) |y{riru*|z||scvnor-|yzu{gvp^{/|z{qk{u1|{zyupsom3{wmktrhl7zooq8clw ⾒(- _// 1Ҧ 3>6ڱ9չh<|P , I - ?. 0/ 0 1237678P , I - ?. 0/ 0 1237678P , I - ?. 0/ 0 1237678LLLL8sN9'mPasted Layer #4!? "     5%$#4qm4    m66:>@"D0D@EGTGdGtJ*OQVI[:[J[Z\^n^~bh|l|q~vvx&zc6Xm<Eǧ48&m IQaq=h[ W  =<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !!!8"8"88"8#8$8$8$8%8%8%8&8&8'8 (8 (8 )8 ) 8 * 8+ 8+ 8, 8=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !!!t"t"tt"t#t$t$t$t%t%t%t&t&t't (t (t )t ) t * t+ t+ t, t=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !![!\"["["[#[$$$[%%[%\[&[&['[ ( ([ )[ )[ *[ + +[ ,[  >==<;::9998776665 5 4 4 4 9638/8- 8+8)8(8&8#8!8 88!8"8$8&8'8)8*8+8,8.8/8 08 18 28 38 485868788898:8:8;8<8=8>88963t/t- t+t)t(t&t#t!t tt!t"t$t&t't)t*t+t,t.t/t 0t 1t 2t 3t 4t5t6t7t8t9t:t:t;ttt96[3[/\[[-\[ +[[)]^[([&[#[![ [[!["[$[&[')s*[+[,[.[/ [0 [1 [2 [3 [4[5[6[7[8[9[:[:^[;<=>[63 /,*'%#!!"$&')*+,./0 1 2 3 4 56789::;<=> %8 8#8'8*8-8/8 28 486888:8<8=8 @8 %t t#t't*t-t/t 2t 4t6t8t:t8<8;8:89868584 83 82 81 808/8.8-8,8+8*8)8(8'8&8&8&8%8$8#8#8"8!8 8 888 8 8!8!8"8"8#8#8$8$8$8%8%8&8>t<t;t:t9t6t5t4 t3 t2 t1 t0t/t.t-t,t+t*t)t(t't&t&t&t%t$t#t#t"t!t t ttt t t!t!t"t"t#t#t$t$t$t%t%t&t[>[<[;:9]6[5[4 [3 [2 [1 [0[/[.[-[,[+[*[)[(['[\&_&&%[$[##"[![ [ [[[g  [![!["["[##[$f$[$[%[%[&=<:9876 4 3 2 10/.-,+*))('&%%$#""!!   !!""##$$$%%&#$%&'()*+,-./01 2 3 4 5 6789:;<=> #$%&'()*+,-./01 2 3 4 5 6789:;<=> #$%&'()*+,-./01 2 3 4 5 6789:;<=> @>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !"@>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !"@>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !" @ @ @ @=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#$%&'()*+,-./0 1 2 3 4 5678=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#$%&'()*+,-./0 1 2 3 4 5678=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#$%&'()*+,-./0 1 2 3 4 5678, 8- 8- 8. 8/ 80 80 81 80 80 8/ 8/8/8/8/8.8.8.8.8/8.8.8-8-8.8-8-8.8.8-8-8.8-8-8-8-8-8-8-8-8-8,8-8-8-8-8-8-8,8-8-8-8-8-8-8-8-8-8,Ќ8-8-8-8-8-8, t- t- t. t/ t0 t0 t1 t0 t0 t/ t/t/t/t/t.t.t.t.t/t.t.t-t-t.t-t-t.t.t-t-t.t-t-t-t-t-t-t-t-t-t,t-t-t-t-t-t-t,t-t-t-t-t-t-t-t-t-t,t-t-t-t-t-t,[ -[ -][ .^ /[ 0[ 0\ 1 0[ 0[ /\[ /`/[/[/[.^.^.[.[/.[.[-\[-\[.-\-[..-[-\.-[-[-\-\-[-\-\-[-[,[-[-[-[-[-[-[,[-[-[-[-[-[-[-[-[-[,[-[-[-[-[-[3 3 3 2 2 2 1 1 1 1 1 0000/////////........................................8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>\>[>[>[>[>[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>J86818 -8+8*8(8&8&8$8$8$8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8Jt6t1t -t+t*t(t&t&t$t$t$t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#t#tJ[[6[^][[1[ [-[b[+[[*[([[&[[&[$[]$[$\[#[#[#]#[#[#]#b\#[#c\#^#[#[#_#\#[#d#e#[#[#e#[#[#q#~]#[#\##[#[##\#[#^##[#J6 1-+*(&&$$$####################################&8&8'8'8'8(8(8(8(8)8)8)8)8*8*8*8*8*8*8*8+8+8+8+8+8+8+8+8+8,8+8+8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8&t&t't't't(t(t(t(t)t)t)t)t*t*t*t*t*t*t*t+t+t+t+t+t+t+t+t+t,t+t+t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t&[&['['\'[(\([(]([))[)[)[^**[*[*[*[*[*[\+\++[+[+[c+[+[+[`+[a,+[+[\,,,\,,,,\,,,\,,,^,\,,,\,,,\,,,,\,,,\,,,,\,&&'''(((())))*******+++++++++,++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#$%&'()*+,-./01 2 3 4 5 6789:;<=> #$%&'()*+,-./01 2 3 4 5 6789:;<=> #$%&'()*+,-./01 2 3 4 5 6789:;<=> @>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !"@>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !"@>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !" @ @ @ @9:;<=)8 88"8%8(8*8,8.8 08 18 28 48687888:8:8;8<8=8>8~8>8~8>8>8>8>8>8~8>8>8>8>8>8~8>8>8>8>8>8>8>8>8>89:;<=)t tt"t%t(t*t,t.t 0t 1t 2t 4t6t7t8t:t:t;tt~t>t~t>t>t>t>t>t~t>t>t>t>t>t~t>t>t>t>t>t>t>t>t>t9:;<=[^[][\`)[[\[ [[h[[^["[[%[\[([\*[[,[[.] 0[ [1[ [2[ [4[6[[7[8:[:[;[<[=[>[~[>[~[>[>[>[>[>[~[>[>[>[>[>[~[>[>[>[>[>[>[>[>[>)" "%(*,.01 2 4 6 78::;<=>~>~>>>>>~>>>>>~>>>>>>>>>-8,8-8-8-8-8-8-8,8-8-8-8-8-8-8-8-8-8,8-8-8-8-8-8-8,8-8-8-8-8-8-8-8-8-8,8,8+8*8)8(8'8&8&8%8%8%8$8#8"8"8!8!8 88 888888 8 88-t,t-t-t-t-t-t-t,t-t-t-t-t-t-t-t-t-t,t-t-t-t-t-t-t,t-t-t-t-t-t-t-t-t-t,t,t+t*t)t(t't&t&t%t%t%t$t#t"t"t!t!t tt tttttt t tt-[,m[-[-[-[-[-[-[,t[-[-[-[-[-[-[-[-[-[,g[-[-[-[-[-[-[,d[-[-[-[-[-[-[-[-[-[,k[,[+[*[[)[[([]'[[&`[[&[[%[[%[%[$[[#["k[["[[![!\ \[ \[[\_[[ _ h....................................-,++*))(''&%%$$#"!!!     >8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>[>>[>[>[>[>[>[>h>[>[>[>[>[>[>[>[>[>>[>[>[>[>[>[>>[>[>[>[>[>[>\>[>[>`>[>[>[>[>[>[>>[>[>\>[>[>[>_>[>[>]>[>[>[>\>[>[>>[>[>\>\>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#8#8#8#8#8#8"8#8#8#8#8#8#8"8#8#8#8#8#8#8"8#8#8"8#8#8#8#8#8#8"8#8#8#8#8#8#8"8#8#8"8#8#8#8"8#8#8"8#8#8#8#8#8#8"8#8#8"8#8#8#8"8#8#8#t#t#t#t#t#t"t#t#t#t#t#t#t"t#t#t#t#t#t#t"t#t#t"t#t#t#t#t#t#t"t#t#t#t#t#t#t"t#t#t"t#t#t#t"t#t#t"t#t#t#t#t#t#t"t#t#t"t#t#t#t"t#t#t[##\#[##d#["\##[#[##\#["]##[#[##\#["##["\##\#[##]#["\##[#[##\#["a##["\##\#["#`#["\##\#\##\#["^##["\##\#["########"######"######"##"######"######"##"###"##"######"##"###"##,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,,[,,,\,_,,,[,,,\,,,,\,,,[,,,],\,,,[,,,\,,,,\,,,[,,,a,\,,,[,,,\,h,,,[,,,[,,,,\,,,[,,,\,],,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,<88;88878684 88>88#$%&'()*+,-./01 2 3 4 5 6789 - t,t)t&t%t%t#t t t!t#t$t&t't(t)t+t,t-t.t/t0t 1t 2t 3t 4t 5t6t7t8t8t9t:t;tt>tt#$%&'()*+,-./01 2 3 4 5 6789[]e- [,[\[)[]&[\%[%[#][ [ [![#`$[&['[([)[+[,[-[.[/[0[ 1[ 2[ 3[ 4[ 5[6[7[8[8[9[:[;[<[=[>[>[ 3/,)'$" !#$&'()+,-./01 2 3 4 5 67889:;<=>>+*)('&%$# " !     8! +*)('&%$# " !     t! +*)('&%$# " !    \ \ !  +>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8|pqqqpprrqqrrpqrprrrqrrqrrpsprrqsppsrrspps?l>pq>l~m>oun>n~m>sp>m~m>pt>l~m>n~l>uo>>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t?>>~>>~>>~>>~>~>>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>[>a>grqhgqmhrrhsqmnqrnrqnmqrnqqmsqmmqqnrqllpqmqqmrprmqpmqprrpqmqprrp?Qt>QpQp>Qt~Qs>QrQmQr>Qr~Qs>QnQq>Qs~Qs>QqQn>Qt~Qs>Qr~Qt>QmQr>Q>>>>>>>>>>>>>>>>>>>>>>>}>>~>>>>>>>>>>>>>>>>>>>>>>~>~>>>>>8!8!8!8!8!8!8!8"8"8"8"8"8"8"8"8"8"8"8"8"8"8"88rpsspqsqpsrprtqqwowrqutqturrutruursms"os"os!mt!mt"ot"ot!lt"ot"ot!nt"ot"vr"qt!mt"vt"vt!mu"vt"vs"nu"nu"vt"rt!mt"rt"rt"mt"nt"vs"vt!nt"vt"vt!nt"st"vs"pt"nt"vtt!t!t!t!t!t!t!t"t"t"t"t"t"t"t"t"t"t"t"t"t"t"tt""""""""""""""""""""""""""""""!""""""""[!![_![!\![![![\"""[^"\_"^"_"[]"\^"\^"[\"[\"\\"[\"[\"\\qspprqprrpqsqprrmq\UmqqooqpoqqopqooqpQYsn"Qqn"Qqn!QTsn!QTsn"Qqm"Qqm!QTsm"Qqm"Qqm!QTrm"Qqm"Qlo"Qpm!QTrn"Qlm"Qlm!QTrm"Qlm"Qln"Qrm"Qrm"Qlm"Qom!QUsm"Qom"Qom"Qrm"Qrm"Qln"Qmm!QUsm"Qlm"Qlm!QUsm"Qom"Qln"Qqm"Qsm"QlmQ!!!!!!!""""""""""""""" p""!!""!""!"""!""!""""""!""""""!""!""""">8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8>8vsruurtvssvtruvrsvtsvvstvssvvsvwstwusvvsuwtsvvsuwttwvsvwtuxutzp>p>s=}>w>q==y>f=k=s>===>======p==p=p=n=p=q=p=q=q=q=o=q=r=p=r=r=r=q=r>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>t>>~>>~>>>>>>>>>>>>=>==================>[>[>>[>[>^>\>[>\>_>\>\>>\>\>]>^>\>\>>]>\>boppnoqonppnopnnppnopnnponppnoponppnopnnpomoommonmoomnommoomnojp_>Qr>Qp>Q{>Qm>Qr=QR>Qo>Q>Q>Q>Q>Q=QR>Q>Q=QR=QR>Q=QR=QS=Qq=QR=Qq=Qp=Qr=Qp=Qp=Qq=Qp=Qp=Qp=Qq=Qo=Qo=Qq=Qo=Qo=Qo=Qp=Qo>>>>>>>>>>>>>>>>>>>>>>><============g=h=g==h=5=g=5=5==4=================="8#8#8#8"8#8#8"8#8#8"8#8#8#8"8#8#8"8#8#8#8"8#8ێszuxxuvyvuxxuwxvvxwuxyvwywvxxvwyvvyxvq"w##u#u##w#vp"w##u"u##v#v"##up"v##v#u##v#uo"w##v"u##v#vo"##vo"u##u#v"#"t#t#t#t"t#t#t"t#t#t"t#t#t#t"t#t#t"t#t#t#t"t#t"######"#########"######"######"##"#####["\##\#\["#]#["]##\["\##\#\["y##\["\##\#\["#_okommonlmnllnmlnnlmnllmmkmnllnmlmmlllp"Qm#Q#Qm#Qm#Q#Qm#Qlq"Qo#Q#Qm#Qm#Q#Ql#QlR"Q#Q}#Qmq"Qm#Q#Ql#Qm#Q#Qn#Qmr"Qm#Q#Ql#Qm#Q#Qn#Qlq"Q#Q#Qlq"Qn#Q#Qm#Ql#Q#Q"###"##"##"###"##"###"#!"##""#""##g""#"4"##""#""##"##g""##"#""##"g"#,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8vxywwzxwyywxzxwzyvyzxxzywyzwxzxwzzwyzyx{zxz|x+++o+++o+,++o,++o+,+o+p,++o+++o+,++o+++n+,+o+n,++n+,+n,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,t,,+,,+,,++,,+,,++,,+,,++,++,,+,,,+,,+,,+,,,[,,,\,,,,[,,,[,,,],\,,,[,,,\nllmmklmkkmlklmkkmkkllkkmkkmkjkljjlkjkljklkij+QS+QS+Q]q+QR+QR+Q]q+QS,Q+QV+Q]q,Q+QR+Q^q+QS,Q+Q]q+QWr,Q+QS+Q^q+QR+QR+Q^r+QV,Q+QW+Q_r+QR+QR+QXr+QR,Q+QTs+QXr,Q+QR+QYr+QR,Q+QXr,,,,,,,,,,,,,,,,,,,,,,,*+++K+++L+,+u+K,++N+,+K+w,++O+++O+v,+w+Q+++}+,++z,+++,+}3 82 81 8.8-8,8+8*8)8(8'8&8%8$8#8"8!8 8!8 88 8 8gqzz|{z||z{|{z||z{|{z}}{|}{{}|{}}{|"#tp"yv#y$%%n&'p'v'j(o(io)**+m+rp+s,,-o--o.mp./p.l/ v/0 t0 1 m1 o1 o1 2 x1 ts1 23 t2 t1 t.t-t,t+t*t)t(t't&t%t$t#t"t!t t!t tt t t"#"#$%&&'''())*++++,---.../// /0 0 1 1 1 1 2 3 1 23[ 2 1 ._-[,[+[*[)[(['[&[%[$[#["[![ [!  \ wqjjiikjijjiijhhiihiihijihiihijihih"Q#Qnq"Qil#Qi$Q%QS%Qr&Q'Qp'QlV'Qv(QrS(Qyq)Q*QR*Q+Qr+Qoq+Qp,QS,Q-Qq-QR-Qq.Qp.Q/QqS.Qs/Q l/Q0Q m0Q 1Q s1Q q1Q q1Q 2Q lR1Q mn1Q 2Q4 2 1 0/.-+*)('&%$$#"!   ""R"#$%k%&'';'(()*4*+++,6,--4-.././/00 0 0 1 1 1 41 1 28}|}}{}~||}}{}}|}~}|~~|}~}|~~|~~}}~~}~~}~}zz}~z~~z~}~z~z~zz~ qunlqslnvmmuplovmmvt hihhihihhihhihghghhgghgghh gpgpggpggfogogoog QoRQlrtpntslstmqtrlsslg8z~zz~zz~zzzzz aaaaa olqumnvnms4t § 4ogoofofononoon oooo QrtpmsslrsoU4Q 48 aP8P8P8P8P88P8P 8>q==<;x:vz9uv8st7qs6p5n5l3k 2l t ttttttt t>=<;:9876654 3 환 QR>Qp=QR=Q==<;:9876O55 3 2 8!8!8!8!8"8"8"8"8!8$8#8#8"8n!8 8 8 8 8 8 8 8Y 8 8 t  z  s  s  s  ~! xs&'vw&st'pq(m))v(t(('&%$#" !   t!t!t!t!t"t"t"t"t##$"#$"#$#"tk"#"t6##t##tb"#t;!#t[%#" tD # t1# tC#" t$#" tf# t1#! th#! tC#$ tO$  t  T > W ~+ k  *w-6 i' V!{ ٽ$! #'  .*&'&'())((('&%$#" !   ![![!`!["["["[a"\]]^_]^_^]^_^]]^^__^]^]^\]#^_\#^`]"^_\]!^`\!^_[!^` \ ^_ [\ ^ \]^` \^_ \]^_ ]^_ ]^` \^] bZXYXYYXXYZYXXYYXYYXYU QpYCEGECGFBFGDDGFBFGDEGECFFCFGEM QhADI QqOADH QgGACI QvMACI Q]EAEI QqSADJ Q_ACJ QhEADJQrXBADHQhDACGQrG ADGQpWD ADHQkM!ADHQZ DC DCDCCDCEHQuLMIIMKIMOHILHISQKNOJJMKHKNIJNLJOOIJMHKQnOORMVPOPOWVNQS&Q'Qlm&Qn'Qqo(Qs)Q)Ql(m(('&%$#" !   **)(('&&"ψuuoqyyokqlgkqxgdu)***u***|*|***x******************ڛ둆ீ舎φٺ󫈒%&'((8)**m~m>pq>m~n>n>l>n>>p>r>s>s>t>u>u>v==r=u=p=v==rr<~p<o<t<<sq;q;u;;ts:l:s9:t9t8s8q78q77ws6r56s45u4tp3k3vo2s11 1 t0 / / v.~>>~>>>>>>>>>>>>>>=====<<<<<;;;;::::998787766645433212 1 0 / / .s~Qs>QqQo>Qs~Qr>Qr>Qs>Qr>Q>Qp>Qo>Qn>Qn>Qm>Qm>Qn>QlR=Q>Qo=Qm=Qq=Ql=Q=Qoo>>>>>>>>>>>>>>>>>>>>h=g====== <<<<<;;;;::m999887877656 4 5 4 3 3 2 U1 g1 10=//.vt!mt"st"st"ot"ot"vt"vt!ns"vs"vs"os"sq"uo"w"v$pr#suj%q%us'qwv(rx*quv- rt0upuwtj2qqsrnq !""""""!"""""#$$#%&')+. 02 Qlm!QUsm"Qnn"Qnn"Qrn"Qrn"Qln"Qln!QUsn"Qln"Qln"Qqn"Qop"Qm{r"QSmR#Ql$QqoW#Qomu%QpW%QRmn'Qpml(QTon*QTpml-QT on0QmqlmnuV2QTQQppnorpQU Q!""""""!""""""64"$#%%'(*-m0 2g =s=p=s=s=r=s=s=t=q=t=t=r=t=u=u=r=v=v-,,,+*)(('&%$#"!  !"#$%&'()*+ , - . /012 3 4 5 6789:;<=>mm-Qoq,Qr,Q,Ql+QmR)QoT(Qq(QS'Q'Q&Q%Q$Q#Q"Q!Q QQ Q!RQ"SQ#pSQ$nQ%lQ&tZQ'qpQ(omQ)lSQ*oQ+mQV Q,pq Q-mwX Q.pl Q/npQ0lsoQ1mjTQ2 lqn3 m4 5 6789:;<=>-.,,,+)((k'&&%$#"!  !"k#%&() *,-/ 0 3z 5 69;7> ?ot9vzsl1rpvuqr*ulqv stpsvvsttrmttuosuuupnbqnuuottqquuovvorrtz(>=<;:9876 5 4 3 2 1 @;4* %(>=<;:9876 5 4 3 2 1 ?QSsm9QSQltntS1QSQpqlmoQo*QUmtom pmRQSQSQRqnlnpmmosnQQ[SQSnQQmqnllopr}prmmqmnppmlrllrprnq(>=<;:9876 5 4 3 2 1 ?9kg61 *q4 Fg-w,uv+su*pr)n)i)'y&u%t$r#q"p"! qr s!t"u#gv$pm&tr'xv(+r,x- rp/ pv0 qo2vs3it4vr8rw9vqv@0/.-,+*)('&%$#"!  !"#$%&'()*+,-./01 2 3 4 5 6789:;<=>@0/.-,+*)('&%$#"!  !"#$%&'()*+,-./01 2 3 4 5 6789:;<=>@ >=<;:987 >=<;:987 >=<;:987"vr#p$$s%%u%sp&j&vp'r())t*+*yv+ts,rq-o.//v0s 1q 2p 3 4 56789:;<=q>r@"#$$%%%&&'())*+*+,-.//0 1 2 3 4 5 6789:;<=>@"loQ#qQ$Q$oXQ%Q%mQ%oqQ&uQ&lqQ'obQ(Q)Q)nQ*WQ+Q*olQ+mnQ,opQ-rUQ.Q/Q/lQ0nW Q1pT Q2q Q3S Q4R Q5RQ6Q7Q8Q9Q:Q;SQo@#$$%%&&'((^()**=+,,-'../00 1 2 3j 4 56789:;k<8=>@ >t>w=np:rs9u8w|6t5 v2 rq1 vk/rt.xs,vnr)t'vr$vos"zunupr"{su'vrurm,utqlrlmutqprt @>=:9885 2 1 0.,)($""', QR>Qm>Qm=Qrq:Qon9Qm8QmrU6Qn5Q lQ^2Q p1Q luT/Qon.Qnn,Qlro)QnU'QlRo$Qlro"QsmsSQlqQoQ"qnlQSQ'lomosRQQZQ,nnptossmQQnpRqoRRn >>=5: 98p65 2 1/.,)9'4$"" &k,45g jiq=r)T*4.c3Q3a3q4669?AHF@K)KM\OTU[ e!ksDz4/OJ'і/#cRh"p\q   =<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !!!"""#$$$%%%&&' ( ( ) ) * + + , =<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !!!"""#$$$%%%&&' ( ( ) ) * + + , =<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !![!\["["["[#[$[$[$[%[%[%\[&[&['[ ([ ( [ ) [ ) [ * [+ [+ [, [ >==l<<;:C:99#98/7g76665 5<44 4s 963/ -+)(&#!  !$%'()*-./0 1 2 3 4 56789:;<<<=?963/ -+)(&#!  !$%'()*-./0 1 2 3 4 56789:;<=<=?96[3[/\[ [-\[+[)]^[([&[#[![ [ [![$[%['[([)[s*[-[.[/[0[ 1[ 2[ 3[ 4[ 5[6[7[8[9[:[;[<[^<[<[=[?["6%83 #V/I , M **'&a%#*!^IY O!<#$%-'o(!)2,?-M. ]/ o0 !1 '2 34u5 h6]7R8@9):: ;R;#< =>? % "$*,/1 3 689<= % "$*,/1 3 689<= ][[ [\[[\\%[\[\ "[$[[_*[,[o/[1[ 3[ 6[8[9[e<[=[ [A'"'L85WV/-*&Vbb%̀JJ  M)%;(| *ٵ*.x' 0. 3>5k6; 8*;g" ><;:96 5 4 3 2 10/.-,+*)('&&&%$##"!     !""###$$%%&&&><;:96 5 4 3 2 10/.-,+*)('&&&%$##"!     !""###$$%%&&&[>[<[;[:[9[]6 [5 [4 [3 [2 [1[0[/[.[-[,[+[*[)[(['[\&[_&[&[%[$[#[#["[![ [ [ [ [g [!["["[#[#[#[$[$[f%[%[&[&[&[#=Ϧ <:89h8+7F6^4 o3 u 2 1 0 /&.!-,|+l*])P)E(0'&]%*%$#")"!l! V6 o !8!"""%##$2$#$%&'()*+,-./01 2 3 4 5 6789:;<=> #$%&'()*+,-./01 2 3 4 5 6789:;<=> #$%&'()*+,-./01 2 3 4 5 6789:;<=> @>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !"@>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !"@>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !" @ @ @ @=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#$%&'()*+,-./0 1 2 3 4 5678=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#$%&'()*+,-./0 1 2 3 4 5678=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#$%&'()*+,-./0 1 2 3 4 5678, - - . / 0 01 00/////..../..--.--..--.---------,------,---------,-----, - - . / 0 01 00/////..../..--.--..--.---------,------,---------,-----, [- [-] [.^ [/ [0 [0\ [1 [0[0[/\[/`[/[/[/[.^[.^[.[.[/[.[.[-\[-\[.[-\[-[.[.[-[-\[.[-[-[-\[-\[-[-\[-\[-[-[,[-[-[-[-[-[-[,[-[-[-[-[-[-[-[-[-[,[-[-[-[-[-[3 3+ 3W 2 2 2k 1 1 1 1 1 0 0 0F 0 /y / / / /./F///...... ..............'...*.#..&.0.!.#.?.).%.6.8.'.0.P./.-.J[\@[?>>B=#====k=|=x=^=g=@=>=8=9<5=6=5<0M;4 1-+*('&%%$$$#$$##$##$$#$$##$$#$$##$$#$$#$$##$M<4 1-+*('&&%$$%#$%#$$%#$%#$$##$$#$$#$$$#$$#$$$#$M[;[^]4[ 1[b-[+[*[(['[&[]%[$[\$[$[$[]#[$[$[]#[b\#[$[c\#[^#[$[$[_#[\#[$[d#[e#[$[$[e#[$[$[q#[~]#[$[\#[#[$[$[#[\#[$[^#[#[$[:W5-#Ra1  b. x+ٵ2)b'|&6$q$.#M""u"@"! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!''((())())****+++++,,+,,,,,,,,,-,,-,,,-,,-,,,,,,-,,-,,,-,,-,,,-,''((()))))**+*+++++,,+,,,,,,,,,-,,-,,,-,,-,,--,,-,,-,,,-,,-,,,-,'['[(['[\([([\)[([])[)[*[*[*[^*[+[+[+[+[+[+[\+[\+[,[,[,[c,[,[,[`,[a,[,[,[\,[,[,[\,[,[,[,[\,[,[,[\,[,[,[^,[\,[,[,[\,[,[,[\,[,[,[,[\,[,[,[\,[,[,[,[\,[$ %k%%&R&&'8'd'' 'B(Y((( (6)B)))) )*8*8*O)|*a*l)**********************************#$%&'()*+,-./01 2 3 4 5 6789:;<=> #$%&'()*+,-./01 2 3 4 5 6789:;<=> #$%&'()*+,-./01 2 3 4 5 6789:;<=> @>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !"@>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !"@>=<;:9876 5 4 3 2 10/.-,+*)('&%$#"!  !" @ @ @ @9:;<=) "&(+,./ 2 3 5 6789:;<=>@9:;<=) "&)+,./ 2 3 5 6789:;<=>@9:;<=[^[][\`)[\[ [h["[^&[([\*[\,[.[/[] 2[ 3[ 5[ 6[7[8[9[:[;[<[=[>[@[     )qȯgk!"% 蕋 | W-"W$d*'&),d-? |/y1 0̕ 2 34F5l6 y7 8:y: ;<=|===x===y====u===u==<=u===s===x=x=-,------,---------,------,---------,,+*)('&&%%%$#""!!       -,------,---------,------,---------,,+*)('&&%%%$#""!!   ! ! -[,m[-[-[-[-[-[-[,t[-[-[-[-[-[-[-[-[-[,g[-[-[-[-[-[-[,d[-[-[-[-[-[-[-[-[-[,k[,[+[*[)[([]'[&`[&[%[%[%[$[#["k["[![![\ [\[ [\[[\[_ [ [ [_ [h[.>./.?.W.5.8.h.B.8.R.[.;.F..I.@.l.^.C.Y..M.O..b.M.o..V.^..g.Y...]-sl,J+k+h *))h0(|'@'q&xg%%$u)$#-"yg!!d! 2 [   ~~~[[h[[[\[`[[\[_[][\[[\>[\=1=0=.<)=+=*<#=&=)=#==%="<< .#!'*. 1 458:> .b_\\\[\b[[[#\[g\![]&[]*[.[ 1[ ^3[`a5[`8[:[>[ p ! #**Pg^q .Ch﹵ 2k ^y$0R( *, u052>k5#7&9@<2 /)'$" "#%&)*+-./022 /)'$" "#%&)*+-./02]\[2\\ [/[)z]['c[$^["\[ _[^["[#[%[v&[)[*[+[-[.[/[0[m^\2[[;3 !-@.Os*8[ ( &%&#J +x +8C!" x$!% J(6)l*,- . #$%&'()*+,-./01 2 3 4 5 6789 -,)&%%# !"#&'()+,-./01 2 3 4 5 67899:;<=>#$%&'()*+,-./01 2 3 4 5 6789[]e-[,[\[)[]&[\%[%[#[][ [!["[#[`&['[([)[+[,[-[.[/[0[1[ 2[ 3[ 4[ 5[ 6[7[8[9[9[:[;[<[=[>[[&3Œ8#/ R* ,)B'*$"0 x-̹ !h"*%C&h'>(*+,-./ 0 1 2 3 4 5677l8@9%:;k>+*)('&%$# " !     ! +*)('&%$# " !     ! +*)('&%$# " !    \ \ [!   +|pqnnqonqpnproorqoqroorpoqroproorroqroorqorropsporroqsppsrorsppskf:abale=ape=akf=akf=aqe=ale=akf=ame=aoe=alg=alf=aue=alf=alg=anf=anf=alg=amf=ase=alg=alg=ape=amf=alg=amf=ape=alg=alg=ate=alf=akg=amf=anf=akg=alf=aue=amg=alg=aof=aם========================================[a^^]^]^^]^]^^]^^]]^^]^^]]^]]^^]^^]]^^]^^]]^]]^^]]^]]^]^]]^grqtsqstrrtsqstqrtrqtsqrtqqssqssqqtrqrrpqsqqsrprsqpsqprrpqsqprrpux=|ty=|py=|tx=|tx=|py=|ty=|ux=|sy=|ry=|tx=|tx=|my=|sx=|tx=|ry=|ry=|tx=|sx=|ny=|tx=|tx=|qy=|sx=|tx=|sx=|qy=|tx=|tx=|ny=|tx=|tx=|sy=|ry=|tx=|tx=|my=|tx=|tx=|ry=|==q===q=y=<y=q===o===s=q===k===y==========================================!""""""""##""#""##"#$싊rpsspqsqpsrprtqqwoۍnwrqutqturrutruursbbaabbabbaabaabcmmsjabbababbababbabkoskcaiosjcablmtibablmtibaiotjcabjotjcabllti ajotjcajotjcablntibabjotibaivricabjqtibablmtibaivticaivthcablmuiabjvtibaivshcabknuibabknuibaivthbajrthbablmth ajrthbairthbabkmtibabkntibaivsgbabjvthbablnth aivthbaivthbablntiabjsthbaivsgbabkpthbabknth aivtgba!"#"""#""#$##$$$$$$$$             [!["[_"[![\"["["[\"["[#[^"[\_"[^"[_#[]"[\^"[\^#[\#[\"[\\#[\#[\[]^]^]]^]^\\[]\\]]\]]\\]]\]]\\qspprqprrpqsqprrmq\smqqooqpoqqopqooqp|{ssnu|{{||{||{{||{||{{|uqnu{|vqnuz|{tsnv{|{tsnv{|vqmvz|uqmu{|{ssmv |uqmu{|vqmu{|{srmv{|{tqmv{|vlov{|{upmv{|{srnv |vlmv{|ulmv{|{srmv |ulmv{|vlnv{|{trmw{|{trmw{|vlmw{|uomw{|{ssmw |uomv{|vomw{|{trmv{|{trmv{|vlnx{|vmmw{|{tsmw |vlmw{|vlmw{|{tsmw |uomw{|vlnw{|{uqmw|{tsmw |vlmx{| &YdW 0'@@'1W-*M;'"8T+#."[6*)BJ**2&^2)+.OB"*29)a! 02/4]%;/ +;B.T+ .>>~>~>>>ߋ vsruurtvssvtruvrsvtsvvstvssvvsvwstwusvvsuwtsvvsuwttwvsvwtuxutzpabbababbababbabbababbababbababbababbabacbabcdmp[\~[\>[_>[\>[\>[>[\>[\>[]>[^>[\>[\>[>[]>[\]\\]]\]]\\]\]\]\]\]\[boppnoqonppnopnnppnopnnponppnoponppnopnnpomoommonmoomnommoomnojp_|{{||{{|{{|{|{{|{{||{{|{{|{|{{|{|{{|{{|{|{{|{|{{|{{zsr<|ztp<|yt{<|zsm<|zsr<|ys<|yso<|zt<|ys<|yr<|zs<|zr<|yr<|yr<|zs<|yr<|yq<|zr<|yq<|yq<|zq<|zr<|yq<|yp<|zr<|yp<|yp<|zq<|zp<|yp<|yp<|zq<|yo<|yo<|zq<|yo<|xo<|yo<|yp<|xo<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<abl|{t=|tp<|vs;|xu:|zx:|{n9|zlq8|xmm7|xon6|xpo5|xqq4|xsr3|xut2|xtt 1|xst > =!7a65#4;3M 2I 1@ !!!!""""!$##"!       냞gifdihbhieeihbt ab az ae as ag abs ab aasaea ao ae~!ae axajsafm&ae'advw&aest'aepq(ckm)dg)gv(t(('&%$#" !   !!!!""""##$"#$"#$#"k"#"6##׈##ֳb"#΂;!#[%#"صD # 1# C#" ֛$#" ضf# Ո1#! h#! ѝC#$ԹO$ þt  T > W ~+ k  *w-6 i' V!{ ٽ$! #'  .*&'&'())((('&%$#" !   [![![![`!["["["[a"[\]]^_]^_^]^_^]]^^__^]^]^[\]#^_[\#^`[]"^_[\]!^`[\!^_ [!^` [\ ^_ [\ ^ [\]^` [\^_ [\]^_ []^_ []^` [\^]bbccbbcbacbZXYXYYXXYZYXXYYXYYXYUxwyzww{xwyywx{pYCEGECGFBFGDDGFBFGDEGECFFCFGEM |hADI |qOADH |ygGACI |vMACI |y]EAEI |{qSADJ |{_ACJ |hEADJ|rXBADH|zhDACG|rG ADG|vWD ADH|zkM!ADH|yZ DC DCDCCDCEH|uLMIIMKIMOHILHISQKNOJJMKHKNIJNLJOOIJMHK|vnOORMVPOPOWVN|xs&|y'|zlm&|ynn'|yqo(zts)zw)xl(m(('&%$#" !   R**)(("'|&E&"ψuuoqyyokqlgkqxgdu)***u***|*|***x******************ڛ둆ீ舎φٺ󫈒d%&*'L((8)**mf=alg=amf=apf=alg=alg=aqf=amg=alg=anf=anf=alh=anh=ag=api=arj=ash=asib .CO5J8 .I;Ch8 4L 1Mq@ bs [   ]E!#&J#$4&?"(B!*a 6, 8o/gR&2ȾbI0Og [,+***)('&%$#"#  !"#$%&'()* + , - ./012 3 4 5 6789:;<=>mmy,|oqy+|rtz*|wy*|lz*|mt)|ot(|qt'|ru&|sv%|vx$|wz#|z{"|#||{ |y|x|vw| uv|!su|"rt|#pt|$nv{{|%lywz|&tsy|'qpy|(omyy|)lsv|*oxz{ |+mvsz |,pqzy |-mwsx{{|.plwu{|/npuwy|0lsoyx|1mjqwvz2 lqn3 m4 5 6789:;<=>,.+**)(J'q&% $$##"! ! " l#*%&(g) s*5,h- / 0 k 3"M5 y695; >adjgbb6abcehmotikefb1acggnnvzslohkedb'abacdhlirpvuqlrjhfddbabedhinulqv stomkhlhfdbabbabaabbabcdggilhmpsvvsttrmtmkolijjggfefefgijimtlnuosuuupnbqnuuottqquuovvorrtz(>=<;:9876 5 4 3 2 161) (>=<;:9876 5 4 3 2 1|zvw{{6|{ywssmvuyy{1|{xxssltntqwuyz{'|{|zzwsvpqlmotouwxzz{|{yzvvrmtom pmqstwtwyy{|{{ |{|{zzwxvtwsqnlnpmmosnsuqtvuuxxyxvuvsntrmqnllopr}prmmqmnppmlrllrprnq(>=<;:9876 5 4 3 2 1=8k7/ ;.Y Pյ'/  *>Ւx 5h^& &"*%+*8*4+*'*?*#* *>*/+*/*@*!*#*B**+!*E*Y+J++,g, -2-./ 0 1& 3R41!  +6A vpd2a vld2a ke2a vmd2a vnd2a ke2a xke2a vqd2a wke2a kf2a vne2a vne2a kf2a wle2a vqd2a le2a ke2a vod2a wle2a kf2a wle2a vod2a ke2a ke2a vpd2a wle2a kf2a vld2a uoc2a jb2a sj3a mf3avqd3and4armf4atsi5avth6atf7aqrh8avpli9aslpce:ajdca 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 334456789: lqz2| mtz2| uy2| lsy2| lry2| ty2| nty2| lpz2| mty2| ty2| lry2| lsy2| ty2| mty2| lpz2| ty2| ty2| lrz2| mty2| uy2| mty2| lrz2| ty2| ty2| lqz2| mty2| ty2| ltz2| mrz2| u{2| nv3| sx3|lpz3|rz4|pry4|nw5|nmw6|nmy7|pnw8|lqsv9|ntpzy:|uzz| 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 -3 3 3 L44b506768#s9T:Rak#abm#acoo"abl#ak#acoo"abn#ak#abm#acoo"al#abl#acoo"abm#ak#acnp"acnn"ak#abl#acnn"abl#ak#acnn"abmo"aj#abl#acnn"abl#al#acqq"acp#abm#adq#adtt"adm#afn#afvw"ahl#alp#ait#ackt#adnv#ael$ahq$aet$abit$abkv$adn%afuw$ajn%abps%aej&aiq&abgv&adov&acl'aju'abk(afvw'abjp(ackv(abiq)agv)agrs)##"##"###"##"##""##"##""##"##"###"##"#####$$$$$%$%%&&&&''('(()))|t#|{s#|{qq"|t#|t#|{qq"|{r#|t#|{s#|{rr"|u#|u#|{rr"|{t#|u#|{ss"|{rr"|u#|{t#|{rr"|{t#|u#|{rr"|{ss"|u#|{t#|{rr"|t#|t#|{pp"|{r#|{t#|zp#|zmn"|zt#|ys#|ylm"|wt#|sp#|vm#|ztm#|zrl#|yt$|wp$|ym$|{vm$|{ul$|zr%|xln$|vs%|{qn%|yu&|vp&|{xl&|zrl&|zt'|ul'|{u(|xlm'|{uq(|{tl(|vp)|wl)|xop)##%########&###&### #%###*###&#"###*##########|$$V$$$$$|$O%C% %%b&R& &&q'2' ''6((l)F)))**?=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#?=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#?=<;:98765 4 3 2 1 0/.-,+*)('&%$#"!  !"#$%&'()*+,-./0 1 2 3 4 56789:;<= $%&'()*+,-./0 1 2 3 4 56789:;<= $%&'()*+,-./0 1 2 3 4 56789:;<=  @ @ @ @0/.-,+*)('&%$#"!  !"#$%&'()*+,-./01 2 3 4 5 6789:;<=>@0/.-,+*)('&%$#"!  !"#$%&'()*+,-./01 2 3 4 5 6789:;<=>@0/.-,+*)('&%$#"!  !"#$%&'()*+,-./01 2 3 4 5 6789:;<=>@ >=<;:987 >=<;:987 >=<;:987"vrfa#pkca$kba$smba%jda%ufa%spfa&jea&vpca'roda(mhba)fa)tja*oha+gba*yvca+tsea,rqea-olda.jgb a/eb a/vkb a0smb a1qlb a2pkb a3nkba4mjba5liba6jga7hgba8jhba9kiba:ljbaa;nkbar@"#$$%%%&&'())*+*+,-.// 0 1 2 3 456789:;<=>@"loy|#qu{|$u{|$os{|%uz|%my|%oqy|&ux|&lq{|'orz|(sw{|)x|)nv|*qw|+w{|*ol{|+mny|,opy|-rsz|.ux{ |/y|/lu |0nt |1pt |2qt{ |3ru |4sv|5tw|6uw|7vw|8uw|9tv|:su|;ru{|o@#$d$%L%&.&'T((6()*8* +&,C,|-'.q./ 0 09 1V 2q 34 5 6789 : ;u<[=>>@ab>al>atfec;awhke:anpe9arsdb7aueg6aw|nj5atffc2a vknd1a rqce/a vkmhbb,artikbb*axskhf)avnrffab%atoigdb#avorhhbb avosikbdbazunmgjecaupjrihdcca"{sumnigheedbba'vrurmojjnhgifeeddcbcdbbd,utqlrlmunmtqmprnnt >;:98652 1 / -*)'$ "', |t>|myyz;|mwty:|rqy9|onz8|myx6|mrsv5|nyxz2| ltrz1| p{y/| lusw{-|onwu{{*|nnuwy)|lroyx'|nqvwz$|lqoww{{ |lrout{z|smssxvzz|lqvovwz{{|"qnlsrvxwyyz|{|'lomosruvswwvxyyz{|{z,nnptossmrsnpsqosrn>=;:59 O8*6O52 Y1 [/O, q*s)1'$5y  L"s" 2Y&-.a , 4Obț령̾5 aj=ajq. *-*-*:;>. ?  Y :un7fW[{ qljoj~ݾrk]spnlmnʾkr5k~~ykdeffhkInݾrkkImʾko| klݾogz hnɽ}[y fn[y fn[y epɽ}[y dsݾogy k]qʾkoy yjo8ݾrkz ~ʾkr| ~ݾrkll jo7o~|~o7fW[{ Y  Y :un7fW[{ qljoj~ݾrl]spnlmnʾlr5k~~ykdeffhkInݾrlkImʾlo| klݾogz hnɽ}[y fn[y fn[y epɽ}[y dsݾogy k]qʾloy yjo8ݾrlz ~ʾlr| ~ݾrlll jo7o~|~o7fW[{ Y  Y :un7fW[{ qljoj~ݾrl]spnlmnʾlr5k~~ykdeffhkInݾrlkImʾlo| klݾohz hnɽ}[y fn[y fn[y epɽ}[y dsݾohy k]qʾloy yjo8ݾrlz ~ʾlr| ~ݾrlll jo7o~|~o7fW[{ Y 4 44$/ 45a& 48 )3'] 4(e ) 4e 7 4a54e4444a444444a4445e4 7a4 آ)e 4 ]'3) (e 4 &84 /5a4 44$۾^ɽ}^ݾogʾkoݾrkɾkrݽokgo[}[[[}goݽokɾkrݾrkʾkoݾogɽ}^۾^۾^ɽ}^ݾogʾloݾrlɾlrݽolgo[}[[[}goݽolɾlrݾrlʾloݾogɽ}^۾^۾^ɽ}^ݾogʾloݾrlɾlrݽolho[}[[[}hoݽolɾlrݾrlʾloݾogɽ}^۾^$aea$aeaaeaa$ea$e 2`` Pasted Layer!? "     c%$#`` ,u`` M'9'+EG%%##!!!!!!!!!!#!##%%##!!!  G%%##!!!!!!!!!!#!##%%##!!!  G%%##!!!!!!!!!!#!##%%##!!!  ?%385 430" +Vȼ'+S h' ); ʈ((B h( *5ʈ((B h( * 5ʈ((A h( *4ʈ((A h( *4ʈ((A h( *4ʈ((@ i) *4ˉ((@ i( *3ˉ)(? h( *4ʇ((A a!*4o(? ., /r C)I N) N P *1y F'? 0*3u&> f%(3Ў,&> o. (3Ґ.&> p. (3Ґ.&> p.  3Ґ. )754 457)             @            @            @> p. )3Ґ. 7 > p. 5 3Ґ. 4 > p. 4 3Ґ.4 > p. 4 3Ґ.4 > p. 4  9 Ґ.5 O p. 7 Q.)$285 431$  )754()))٢) 7 5 4 4 4 4 4 4 5 7 ٢) 457) J00Layer!? "     %$#--yyyy2K393U3q333334454Q5E8I8Y8i8y8888888;>???!?1?A?Q?a?q??BEEEEEEEF FF)F9I=LALQLaLqLLLLLLLORRS SS)S9SISYSiSySVYYYYYYYZZZ!Z1]5`9`I`Y`i`y``````cffggg!g1gAgQgagqgjmmmmmmmmn nn)q-t1tAtQtatqttttttwx-xIxexxxxxy y)yE O0000000000000000000 O0000000000000000000 O0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0" @ @ @" @ @ @" @ @ @" @ @ @" @ @ @" @ @ @" @ @ @" @ @ @" @ @ @" @ @ @""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"0"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""@0000000000000"0"0"0"0"0"@"@"@"@"@"@"@"@"@"@"@JJ""""""JjjZZ-- Layer #1!? "     %$#sz{  ,<L\l| ,<L\l| ,<L\l| ,<L\l| ,<L\l| ,<L\l| ,<L\l| ,<L\l| ,<L\l|kkZZ--pyTooling-8.11.0/pyTooling/000077500000000000000000000000001513317154500155565ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Attributes/000077500000000000000000000000001513317154500177045ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Attributes/ArgParse/000077500000000000000000000000001513317154500214105ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Attributes/ArgParse/Argument.py000066400000000000000000000246261513317154500235560ustar00rootroot00000000000000# ==================================================================================================================== # # _ _ _ _ _ _ _ ____ # # / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ # # / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ # # _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ # # (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| # # |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # from pathlib import Path from typing import Type try: from pyTooling.Decorators import export from pyTooling.Attributes.ArgParse import CommandLineArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Attributes.ArgParse.Argument] Could not import from 'pyTooling.*'!") try: from Decorators import export from Attributes.ArgParse import CommandLineArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Attributes.ArgParse.Argument] Could not import directly!") raise ex @export class DelimiterArgument(CommandLineArgument): """ Represents a delimiter symbol like ``--``. """ @export class NamedArgument(CommandLineArgument): """ Base-class for all command line arguments with a name. """ @export class ValuedArgument(CommandLineArgument): """ Base-class for all command line arguments with a value. """ class NamedAndValuedArgument(NamedArgument, ValuedArgument): """ Base-class for all command line arguments with a name and a value. """ class NamedTupledArgument(NamedArgument, ValuedArgument): """ Class and base-class for all TupleFlag classes, which represents an argument with separate value. A tuple argument is a command line argument followed by a separate value. Name and value are passed as two arguments to the executable. **Example: ** * `width 100`` """ @export class PositionalArgument(ValuedArgument): """ Represents a simple string argument containing any information encoded in a string. TODO A list of strings is available as :class:`~pyTooling.Attribute.ArgParse.Argument.StringListArgument`. """ def __init__(self, dest: str, metaName: str, type: Type = str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ args = [] kwargs = { "dest": dest, "metavar": metaName, "type": type, "help": help } if optional: kwargs["nargs"] = "?" super().__init__(*args, **kwargs) @export class StringArgument(PositionalArgument): """ Represents a simple string argument. A list of strings is available as :class:`~pyTooling.Attribute.ArgParse.Argument.StringListArgument`. """ def __init__(self, dest: str, metaName: str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__(dest, metaName, str, optional, help) @export class IntegerArgument(PositionalArgument): """ Represents an integer argument. A list of strings is available as :class:`~pyTooling.Attribute.ArgParse.Argument.StringListArgument`. """ def __init__(self, dest: str, metaName: str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__(dest, metaName, int, optional, help) @export class FloatArgument(PositionalArgument): """ Represents a floating point number argument. A list of strings is available as :class:`~pyTooling.Attribute.ArgParse.Argument.StringListArgument`. """ def __init__(self, dest: str, metaName: str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__(dest, metaName, float, optional, help) # TODO: Add option to class if path should be checked for existence @export class PathArgument(PositionalArgument): """ Represents a single path argument. A list of paths is available as :class:`~pyTooling.Attribute.ArgParse.Argument.PathListArgument`. """ def __init__(self, dest: str, metaName: str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__(dest, metaName, Path, optional, help) @export class ListArgument(ValuedArgument): """ Represents a list of string argument (:class:`~pyTooling.Attribute.ArgParse.Argument.StringArgument`). """ def __init__(self, dest: str, metaName: str, type: Type = str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ args = [] kwargs = { "dest": dest, "metavar": metaName, "nargs": "*" if optional else "+", "type": type, "help": help } super().__init__(*args, **kwargs) @export class StringListArgument(ListArgument): """ Represents a list of string argument (:class:`~pyTooling.Attribute.ArgParse.Argument.StringArgument`). """ def __init__(self, dest: str, metaName: str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__(dest, metaName, str, optional, help) @export class IntegerListArgument(ListArgument): """ Represents a list of string argument (:class:`~pyTooling.Attribute.ArgParse.Argument.StringArgument`). """ def __init__(self, dest: str, metaName: str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__(dest, metaName, int, optional, help) @export class FloatListArgument(ListArgument): """ Represents a list of string argument (:class:`~pyTooling.Attribute.ArgParse.Argument.StringArgument`). """ def __init__(self, dest: str, metaName: str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__(dest, metaName, float, optional, help) @export class PathListArgument(ListArgument): """ Represents a list of path arguments (:class:`~pyTooling.Attribute.ArgParse.Argument.PathArgument`). """ def __init__(self, dest: str, metaName: str, optional: bool = False, help: str = "") -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__(dest, metaName, Path, optional, help) pyTooling-8.11.0/pyTooling/Attributes/ArgParse/BooleanFlag.py000066400000000000000000000110331513317154500241310ustar00rootroot00000000000000# ==================================================================================================================== # # _ _ _ _ _ _ _ ____ # # / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ # # / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ # # _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ # # (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| # # |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # try: from pyTooling.Decorators import export from pyTooling.Attributes.ArgParse.Argument import NamedArgument, ValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Attributes.ArgParse.BooleanFlag] Could not import from 'pyTooling.*'!") try: from Decorators import export from Attributes.ArgParse.Argument import NamedArgument, ValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Attributes.ArgParse.BooleanFlag] Could not import directly!") raise ex @export class BooleanFlag(NamedArgument, ValuedArgument): pass @export class ShortBooleanFlag(BooleanFlag): #, pattern="-with-{0}", falsePattern="-without-{0}"): pass @export class LongBooleanFlag(BooleanFlag): #, pattern="--with-{0}", falsePattern="--without-{0}"): pass @export class WindowsBooleanFlag(BooleanFlag): #, pattern="/with-{0}", falsePattern="/without-{0}"): pass pyTooling-8.11.0/pyTooling/Attributes/ArgParse/Flag.py000066400000000000000000000133761513317154500226450ustar00rootroot00000000000000# ==================================================================================================================== # # _ _ _ _ _ _ _ ____ # # / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ # # / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ # # _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ # # (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| # # |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # from typing import Optional as Nullable try: from pyTooling.Decorators import export from pyTooling.Attributes.ArgParse.Argument import NamedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Attributes.ArgParse.Flag] Could not import from 'pyTooling.*'!") try: from Decorators import export from Attributes.ArgParse.Argument import NamedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Attributes.ArgParse.Flag] Could not import directly!") raise ex @export class FlagArgument(NamedArgument): """ Defines a switch argument like ``--help``. Some of the named parameters passed to :meth:`~ArgumentParser.add_argument` are predefined (or overwritten) to create a boolean parameter passed to the registered handler method. The boolean parameter is ``True`` if the switch argument is present in the commandline arguments, otherwise ``False``. """ def __init__(self, short: Nullable[str] = None, long: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: """ The constructor expects positional (``*args``), the destination parameter name ``dest`` and/or named parameters (``**kwargs``) which are passed to :meth:`~ArgumentParser.add_argument`. To implement a switch argument, the following named parameters are predefined: * ``action="store_const"`` * ``const=True`` * ``default=False`` This implements a boolean parameter passed to the handler method. """ args = [] if short is not None: args.append(short) if long is not None: args.append(long) kwargs = { "dest": dest, "action": "store_const", "const": True, "default": False, "help": help, } super().__init__(*args, **kwargs) @export class ShortFlag(FlagArgument): def __init__(self, short: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: super().__init__(short=short, dest=dest, help=help) @export class LongFlag(FlagArgument): def __init__(self, long: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: super().__init__(long=long, dest=dest, help=help) pyTooling-8.11.0/pyTooling/Attributes/ArgParse/KeyValueFlag.py000066400000000000000000000135431513317154500243070ustar00rootroot00000000000000# ==================================================================================================================== # # _ _ _ _ _ _ _ ____ # # / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ # # / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ # # _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ # # (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| # # |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # from typing import Optional as Nullable try: from pyTooling.Decorators import export from pyTooling.Attributes.ArgParse.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Attributes.ArgParse.KeyValueFlag] Could not import from 'pyTooling.*'!") try: from Decorators import export from Attributes.ArgParse.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Attributes.ArgParse.KeyValueFlag] Could not import directly!") raise ex @export class NamedKeyValuePairsArgument(NamedAndValuedArgument): """ Defines a switch argument like ``--help``. Some of the named parameters passed to :meth:`~ArgumentParser.add_argument` are predefined (or overwritten) to create a boolean parameter passed to the registered handler method. The boolean parameter is ``True`` if the switch argument is present in the commandline arguments, otherwise ``False``. """ def __init__(self, short: Nullable[str] = None, long: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: """ The constructor expects positional (``*args``), the destination parameter name ``dest`` and/or named parameters (``**kwargs``) which are passed to :meth:`~ArgumentParser.add_argument`. To implement a switch argument, the following named parameters are predefined: * ``action="store_const"`` * ``const=True`` * ``default=False`` This implements a boolean parameter passed to the handler method. """ args = [] if short is not None: args.append(short) if long is not None: args.append(long) kwargs = { "dest": dest, "action": "store_const", "const": True, "default": False, "help": help, } super().__init__(*args, **kwargs) @export class ShortKeyValueFlag(NamedKeyValuePairsArgument): def __init__(self, short: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: super().__init__(short=short, dest=dest, help=help) @export class LongKeyValueFlag(NamedKeyValuePairsArgument): def __init__(self, long: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: super().__init__(long=long, dest=dest, help=help) pyTooling-8.11.0/pyTooling/Attributes/ArgParse/OptionalValuedFlag.py000066400000000000000000000135471513317154500255140ustar00rootroot00000000000000# ==================================================================================================================== # # _ _ _ _ _ _ _ ____ # # / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ # # / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ # # _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ # # (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| # # |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # from typing import Optional as Nullable try: from pyTooling.Decorators import export from pyTooling.Attributes.ArgParse.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Attributes.ArgParse.OptionalValuedFlag] Could not import from 'pyTooling.*'!") try: from Decorators import export from Attributes.ArgParse.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Attributes.ArgParse.OptionalValuedFlag] Could not import directly!") raise ex @export class OptionalValuedFlag(NamedAndValuedArgument): """ Defines a switch argument like ``--repeat[=1]``. Some of the named parameters passed to :meth:`~ArgumentParser.add_argument` are predefined (or overwritten) to create a boolean parameter passed to the registered handler method. The boolean parameter is ``True`` if the switch argument is present in the commandline arguments, otherwise ``False``. """ def __init__(self, short: Nullable[str] = None, long: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: """ The constructor expects positional (``*args``), the destination parameter name ``dest`` and/or named parameters (``**kwargs``) which are passed to :meth:`~ArgumentParser.add_argument`. To implement a switch argument, the following named parameters are predefined: * ``action="store_const"`` * ``const=True`` * ``default=False`` This implements a boolean parameter passed to the handler method. """ args = [] if short is not None: args.append(short) if long is not None: args.append(long) kwargs = { "dest": dest, "action": "store_const", "const": True, "default": False, "help": help, } super().__init__(*args, **kwargs) @export class ShortOptionalValuedFlag(OptionalValuedFlag): def __init__(self, short: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: super().__init__(short=short, dest=dest, help=help) @export class LongOptionalValuedFlag(OptionalValuedFlag): def __init__(self, long: Nullable[str] = None, dest: Nullable[str] = None, help: Nullable[str] = None) -> None: super().__init__(long=long, dest=dest, help=help) pyTooling-8.11.0/pyTooling/Attributes/ArgParse/ValuedFlag.py000066400000000000000000000137601513317154500240030ustar00rootroot00000000000000# ==================================================================================================================== # # _ _ _ _ _ _ _ ____ # # / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ # # / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ # # _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ # # (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| # # |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # from typing import Optional as Nullable try: from pyTooling.Decorators import export from pyTooling.Attributes.ArgParse.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Attributes.ArgParse.ValuedFlag] Could not import from 'pyTooling.*'!") try: from Decorators import export from Attributes.ArgParse.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Attributes.ArgParse.ValuedFlag] Could not import directly!") raise ex @export class ValuedFlag(NamedAndValuedArgument): """ Defines a switch argument like ``--count=25``. Some of the named parameters passed to :meth:`~ArgumentParser.add_argument` are predefined (or overwritten) to create a boolean parameter passed to the registered handler method. The boolean parameter is ``True`` if the switch argument is present in the commandline arguments, otherwise ``False``. """ def __init__(self, short: Nullable[str] = None, long: Nullable[str] = None, dest: Nullable[str] = None, metaName: Nullable[str] = None, optional: bool = False, help: Nullable[str] = None) -> None: """ The constructor expects positional (``*args``), the destination parameter name ``dest`` and/or named parameters (``**kwargs``) which are passed to :meth:`~ArgumentParser.add_argument`. To implement a switch argument, the following named parameters are predefined: * ``action="store_const"`` * ``const=True`` * ``default=False`` This implements a boolean parameter passed to the handler method. """ args = [] if short is not None: args.append(short) if long is not None: args.append(long) kwargs = { "dest": dest, "metavar": metaName, "default": None, "help": help, "required": not optional } super().__init__(*args, **kwargs) @export class ShortValuedFlag(ValuedFlag): def __init__(self, short: Nullable[str] = None, dest: Nullable[str] = None, metaName: Nullable[str] = None, optional: bool = False, help: Nullable[str] = None) -> None: super().__init__(short, None, dest, metaName, optional, help) @export class LongValuedFlag(ValuedFlag): def __init__(self, long: Nullable[str] = None, dest: Nullable[str] = None, metaName: Nullable[str] = None, optional: bool = False, help: Nullable[str] = None) -> None: super().__init__(None, long, dest, metaName, optional, help) pyTooling-8.11.0/pyTooling/Attributes/ArgParse/__init__.py000066400000000000000000000352401513317154500235250ustar00rootroot00000000000000# ==================================================================================================================== # # _ _ _ _ _ _ _ ____ # # / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ # # / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ # # _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ # # (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| # # |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # from argparse import ArgumentParser, Namespace from typing import Callable, Dict, Tuple, Any, TypeVar try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyTooling.Exceptions import ToolingException from pyTooling.Common import firstElement, firstPair from pyTooling.Attributes import Attribute except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Attributes.ArgParse] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType from Exceptions import ToolingException from Common import firstElement, firstPair from Attributes import Attribute except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Attributes.ArgParse] Could not import directly!") raise ex M = TypeVar("M", bound=Callable) @export class ArgParseException(ToolingException): pass #@abstract @export class ArgParseAttribute(Attribute): """ Base-class for all attributes to describe a :mod:`argparse`-base command line argument parser. """ @export class _HandlerMixin(metaclass=ExtendedType, mixin=True): """ A mixin-class that offers a class field for a reference to a handler method and a matching property. """ _handler: Callable = None #: Reference to a method that is called to handle e.g. a sub-command. @readonly def Handler(self) -> Callable: """Returns the handler method.""" return self._handler # FIXME: Is _HandlerMixin needed here, or for commands? @export class CommandLineArgument(ArgParseAttribute, _HandlerMixin): """ Base-class for all *Argument* classes. An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter to specify how argument are formatted. There are multiple derived formats supporting: * commands |br| |rarr| :mod:`~pyTooling.Attribute.ArgParse.Command` * simple names (flags) |br| |rarr| :mod:`~pyTooling.Attribute.ArgParse.Flag`, :mod:`~pyTooling.Attribute.ArgParse.BooleanFlag` * simple values (vlaued flags) |br| |rarr| :class:`~pyTooling.Attribute.ArgParse.Argument.StringArgument`, :class:`~pyTooling.Attribute.ArgParse.Argument.PathArgument` * names and values |br| |rarr| :mod:`~pyTooling.Attribute.ArgParse.ValuedFlag`, :mod:`~pyTooling.Attribute.ArgParse.OptionalValuedFlag` * key-value pairs |br| |rarr| :mod:`~pyTooling.Attribute.ArgParse.NamedKeyValuePair` """ # def __init__(self, args: Iterable, kwargs: Mapping) -> None: # """ # The constructor expects ``args`` for positional and/or ``kwargs`` for named parameters which are passed without # modification to :meth:`~ArgumentParser.add_argument`. # """ # # super().__init__(*args, **kwargs) _args: Tuple _kwargs: Dict def __init__(self, *args: Any, **kwargs: Any) -> None: """ The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without modification to :meth:`~ArgumentParser.add_argument`. """ super().__init__() self._args = args self._kwargs = kwargs @readonly def Args(self) -> Tuple: """ A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are passed without modification to :class:`~ArgumentParser`. """ return self._args @readonly def KWArgs(self) -> Dict: """ A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are passed without modification to :class:`~ArgumentParser`. """ return self._kwargs @export class CommandGroupAttribute(ArgParseAttribute): """ *Experimental* attribute to group sub-commands in groups for better readability in a ``prog.py --help`` call. """ __groupName: str = None def __init__(self, groupName: str) -> None: """ The constructor expects a 'groupName' which can be used to group sub-commands for better readability. """ super().__init__() self.__groupName = groupName @readonly def GroupName(self) -> str: """Returns the name of the command group.""" return self.__groupName # @export # class _KwArgsMixin(metaclass=ExtendedType, mixin=True): # """ # A mixin-class that offers a class field for named parameters (```**kwargs``) and a matching property. # """ # _kwargs: Dict #: A dictionary of additional keyword parameters. # # @readonly # def KWArgs(self) -> Dict: # """ # A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are # passed without modification to :class:`~ArgumentParser`. # """ # return self._kwargs # # # @export # class _ArgsMixin(_KwArgsMixin, mixin=True): # """ # A mixin-class that offers a class field for positional parameters (```*args``) and a matching property. # """ # # _args: Tuple #: A tuple of additional positional parameters. # # @readonly # def Args(self) -> Tuple: # """ # A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are # passed without modification to :class:`~ArgumentParser`. # """ # return self._args @export class DefaultHandler(ArgParseAttribute, _HandlerMixin): """ Marks a handler method as *default* handler. This method is called if no sub-command is given. It's an error, if more than one method is annotated with this attribute. """ def __call__(self, func: Callable) -> Callable: self._handler = func return super().__call__(func) @export class CommandHandler(ArgParseAttribute, _HandlerMixin): #, _KwArgsMixin): """Marks a handler method as responsible for the given 'command'. This constructs a sub-command parser using :meth:`~ArgumentParser.add_subparsers`. """ _command: str _help: str # FIXME: extract to mixin? _args: Tuple _kwargs: Dict def __init__(self, command: str, help: str = "", **kwargs: Any) -> None: """The constructor expects a 'command' and an optional list of named parameters (keyword arguments) which are passed without modification to :meth:`~ArgumentParser.add_subparsers`. """ super().__init__() self._command = command self._help = help self._args = tuple() self._kwargs = kwargs self._kwargs["help"] = help def __call__(self, func: M) -> M: self._handler = func return super().__call__(func) @readonly def Command(self) -> str: """Returns the 'command' a sub-command parser adheres to.""" return self._command # FIXME: extract to mixin? @readonly def Args(self) -> Tuple: """ A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are passed without modification to :class:`~ArgumentParser`. """ return self._args # FIXME: extract to mixin? @readonly def KWArgs(self) -> Dict: """ A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are passed without modification to :class:`~ArgumentParser`. """ return self._kwargs @export class ArgParseHelperMixin(metaclass=ExtendedType, mixin=True): """ Mixin-class to implement an :mod:`argparse`-base command line argument processor. """ _mainParser: ArgumentParser _formatter: Any # TODO: Find type _subParser: Any # TODO: Find type _subParsers: Dict # TODO: Find type def __init__(self, **kwargs: Any) -> None: """ The mixin-constructor expects an optional list of named parameters which are passed without modification to the :class:`ArgumentParser` constructor. """ from .Argument import CommandLineArgument super().__init__() self._subParser = None self._subParsers = {} self._formatter = kwargs["formatter_class"] if "formatter_class" in kwargs else None if "formatter_class" in kwargs: self._formatter = kwargs["formatter_class"] if "allow_abbrev" not in kwargs: kwargs["allow_abbrev"] = False if "exit_on_error" not in kwargs: kwargs["exit_on_error"] = False # create a commandline argument parser self._mainParser = ArgumentParser(**kwargs) # Search for 'DefaultHandler' marked method methods = self.GetMethodsWithAttributes(predicate=DefaultHandler) if (methodCount := len(methods)) == 1: defaultMethod, attributes = firstPair(methods) if len(attributes) > 1: raise ArgParseException("Marked default handler multiple times with 'DefaultAttribute'.") # set default handler for the main parser self._mainParser.set_defaults(func=firstElement(attributes).Handler) # Add argument descriptions for the main parser methodAttributes = defaultMethod.GetAttributes(CommandLineArgument) # ArgumentAttribute) for methodAttribute in methodAttributes: self._mainParser.add_argument(*methodAttribute.Args, **methodAttribute.KWArgs) elif methodCount > 1: raise ArgParseException("Marked more then one handler as default handler with 'DefaultAttribute'.") # Search for 'CommandHandler' marked methods methods: Dict[Callable, Tuple[CommandHandler]] = self.GetMethodsWithAttributes(predicate=CommandHandler) for method, attributes in methods.items(): if self._subParser is None: self._subParser = self._mainParser.add_subparsers(help='sub-command help') if len(attributes) > 1: raise ArgParseException("Marked command handler multiple times with 'CommandHandler'.") # Add a sub parser for each command / handler pair attribute = firstElement(attributes) kwArgs = attribute.KWArgs.copy() if "formatter_class" not in kwArgs and self._formatter is not None: kwArgs["formatter_class"] = self._formatter kwArgs["allow_abbrev"] = False if "allow_abbrev" not in kwargs else kwargs["allow_abbrev"] subParser = self._subParser.add_parser(attribute.Command, **kwArgs) subParser.set_defaults(func=attribute.Handler) # Add arguments for the sub-parsers methodAttributes = method.GetAttributes(CommandLineArgument) # ArgumentAttribute) for methodAttribute in methodAttributes: subParser.add_argument(*methodAttribute.Args, **methodAttribute.KWArgs) self._subParsers[attribute.Command] = subParser def Run(self, enableAutoComplete: bool = True) -> None: if enableAutoComplete: self._EnabledAutoComplete() self._ParseArguments() def _EnabledAutoComplete(self) -> None: try: from argcomplete import autocomplete autocomplete(self._mainParser) except ImportError: # pragma: no cover pass def _ParseArguments(self) -> None: # parse command line options and process split arguments in callback functions parsed, args = self._mainParser.parse_known_args() self._RouteToHandler(parsed) def _RouteToHandler(self, args: Namespace) -> None: # because func is a function (unbound to an object), it MUST be called with self as a first parameter args.func(self, args) @readonly def MainParser(self) -> ArgumentParser: """Returns the main parser.""" return self._mainParser @readonly def SubParsers(self) -> Dict: """Returns the sub-parsers.""" return self._subParsers # String # StringList # Path # PathList # Delimiter # ValuedFlag --option=value # ValuedFlagList --option=foo --option=bar # OptionalValued --option --option=foo # ValuedTuple pyTooling-8.11.0/pyTooling/Attributes/__init__.py000066400000000000000000000312711513317154500220210ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ This Python module offers the base implementation of .NET-like attributes realized with class-based Python decorators. This module comes also with a mixin-class to ease using classes having annotated methods. The annotated data is stored as instances of :class:`~pyTooling.Attributes.Attribute` classes in an additional field per class, method or function. By default, this field is called ``__pyattr__``. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from enum import IntFlag from types import MethodType, FunctionType, ModuleType from typing import Callable, List, TypeVar, Dict, Any, Iterable, Union, Type, Tuple, Generator, ClassVar from typing import Optional as Nullable try: from pyTooling.Decorators import export, readonly from pyTooling.Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Attributes] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Attributes] Could not import directly!") raise ex __all__ = ["Entity", "TAttr", "TAttributeFilter", "ATTRIBUTES_MEMBER_NAME"] Entity = TypeVar("Entity", bound=Union[Type, Callable]) """A type variable for functions, methods or classes.""" TAttr = TypeVar("TAttr", bound='Attribute') """A type variable for :class:`~pyTooling.Attributes.Attribute`.""" TAttributeFilter = Union[Type[TAttr], Iterable[Type[TAttr]], None] """A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an iterable of those.""" ATTRIBUTES_MEMBER_NAME: str = "__pyattr__" """Field name on entities (function, class, method) to store pyTooling.Attributes.""" @export class AttributeScope(IntFlag): """ An enumeration of possible entities an attribute can be applied to. Values of this enumeration can be merged (or-ed) if an attribute can be applied to multiple language entities. Supported language entities are: classes, methods or functions. Class fields or module variables are not supported. """ Class = 1 #: Attribute can be applied to classes. Method = 2 #: Attribute can be applied to methods. Function = 4 #: Attribute can be applied to functions. Any = Class + Method + Function #: Attribute can be applied to any language entity. @export class Attribute: # (metaclass=ExtendedType, slots=True): """Base-class for all pyTooling attributes.""" # __AttributesMemberName__: ClassVar[str] = "__pyattr__" #: Field name on entities (function, class, method) to store pyTooling.Attributes. _functions: ClassVar[List[Any]] = [] #: List of functions, this Attribute was attached to. _classes: ClassVar[List[Any]] = [] #: List of classes, this Attribute was attached to. _methods: ClassVar[List[Any]] = [] #: List of methods, this Attribute was attached to. _scope: ClassVar[AttributeScope] = AttributeScope.Any #: Allowed language construct this attribute can be used with. # Ensure each derived class has its own instances of class variables. def __init_subclass__(cls, **kwargs: Any) -> None: """ Ensure each derived class has its own instance of ``_functions``, ``_classes`` and ``_methods`` to register the usage of that Attribute. """ super().__init_subclass__(**kwargs) cls._functions = [] cls._classes = [] cls._methods = [] # Make all classes derived from Attribute callable, so they can be used as a decorator. def __call__(self, entity: Entity) -> Entity: """ Attributes get attached to an entity (function, class, method) and an index is updated at the attribute for reverse lookups. :param entity: Entity (function, class, method), to attach an attribute to. :returns: Same entity, with attached attribute. :raises TypeError: If parameter 'entity' is not a function, class nor method. """ self._AppendAttribute(entity, self) return entity @staticmethod def _AppendAttribute(entity: Entity, attribute: "Attribute") -> None: """ Append an attribute to a language entity (class, method, function). .. hint:: This method can be used in attribute groups to apply multiple attributes within ``__call__`` method. .. code-block:: Python class GroupAttribute(Attribute): def __call__(self, entity: Entity) -> Entity: self._AppendAttribute(entity, SimpleAttribute(...)) self._AppendAttribute(entity, SimpleAttribute(...)) return entity :param entity: Entity, the attribute is attached to. :param attribute: Attribute to attach. :raises TypeError: If parameter 'entity' is not a class, method or function. """ if isinstance(entity, MethodType): attribute._methods.append(entity) elif isinstance(entity, FunctionType): attribute._functions.append(entity) elif isinstance(entity, type): attribute._classes.append(entity) else: ex = TypeError(f"Parameter 'entity' is not a function, class nor method.") ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.") raise ex if hasattr(entity, ATTRIBUTES_MEMBER_NAME): getattr(entity, ATTRIBUTES_MEMBER_NAME).insert(0, attribute) else: setattr(entity, ATTRIBUTES_MEMBER_NAME, [attribute, ]) @property def Scope(cls) -> AttributeScope: return cls._scope @classmethod def GetFunctions(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]: """ Return a generator for all functions, where this attribute is attached to. The resulting item stream can be filtered by: * ``scope`` - when the item is a nested class in scope ``scope``. :param scope: Undocumented. :returns: A sequence of functions where this attribute is attached to. """ if scope is None: for c in cls._functions: yield c elif isinstance(scope, ModuleType): elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, FunctionType)) for c in cls._functions: if c in elementsInScope: yield c else: raise NotImplementedError(f"Parameter 'scope' is a class isn't supported yet.") @classmethod def GetClasses(cls, scope: Union[Type, ModuleType, None] = None, subclassOf: Nullable[Type] = None) -> Generator[TAttr, None, None]: # def GetClasses(cls, scope: Nullable[Type] = None, predicate: Nullable[TAttributeFilter] = None) -> Generator[TAttr, None, None]: """ Return a generator for all classes, where this attribute is attached to. The resulting item stream can be filtered by: * ``scope`` - when the item is a nested class in scope ``scope``. * ``subclassOf`` - when the item is a subclass of ``subclassOf``. :param scope: Undocumented. :param subclassOf: An attribute class or tuple thereof, to filter for that attribute type or subtype. :returns: A sequence of classes where this attribute is attached to. """ from pyTooling.Common import isnestedclass if scope is None: if subclassOf is None: for c in cls._classes: yield c else: for c in cls._classes: if issubclass(c, subclassOf): yield c elif subclassOf is None: if isinstance(scope, ModuleType): elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, type)) for c in cls._classes: if c in elementsInScope: yield c else: for c in cls._classes: if isnestedclass(c, scope): yield c else: for c in cls._classes: if isnestedclass(c, scope) and issubclass(c, subclassOf): yield c @classmethod def GetMethods(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]: """ Return a generator for all methods, where this attribute is attached to. The resulting item stream can be filtered by: * ``scope`` - when the item is a nested class in scope ``scope``. :param scope: Undocumented. :returns: A sequence of methods where this attribute is attached to. """ if scope is None: for c in cls._methods: yield c else: for m in cls._methods: if m.__classobj__ is scope: yield m @classmethod def GetAttributes(cls, method: MethodType, includeSubClasses: bool = True) -> Tuple['Attribute', ...]: """ Returns attached attributes of this kind for a given method. :param method: :param includeSubClasses: :return: :raises TypeError: """ if hasattr(method, ATTRIBUTES_MEMBER_NAME): attributes = getattr(method, ATTRIBUTES_MEMBER_NAME) if isinstance(attributes, list): return tuple(attribute for attribute in attributes if isinstance(attribute, cls)) else: raise TypeError(f"Method '{method.__class__.__name__}{method.__name__}' has a '{ATTRIBUTES_MEMBER_NAME}' field, but it's not a list of Attributes.") return tuple() @export class SimpleAttribute(Attribute): _args: Tuple[Any, ...] _kwargs: Dict[str, Any] def __init__(self, *args, **kwargs) -> None: self._args = args self._kwargs = kwargs @readonly def Args(self) -> Tuple[Any, ...]: return self._args @readonly def KwArgs(self) -> Dict[str, Any]: return self._kwargs pyTooling-8.11.0/pyTooling/CLIAbstraction/000077500000000000000000000000001513317154500203575ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/CLIAbstraction/Argument.py000066400000000000000000000636171513317154500225300ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ This module implements command line arguments without prefix character(s). """ from abc import abstractmethod from pathlib import Path from typing import ClassVar, List, Union, Iterable, TypeVar, Generic, Any, Optional as Nullable, Self try: from pyTooling.Decorators import export, readonly from pyTooling.Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex __all__ = ["ValueT"] ValueT = TypeVar("ValueT") #: The type of value in a valued argument. @export class CommandLineArgument: """ Base-class for all *Argument* classes. An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter to specify how argument are formatted. There are multiple derived formats supporting: * commands |br| |rarr| :mod:`~pyTooling.CLIAbstraction.Command` * simple names (flags) |br| |rarr| :mod:`~pyTooling.CLIAbstraction.Flag`, :mod:`~pyTooling.CLIAbstraction.BooleanFlag` * simple values (vlaued flags) |br| |rarr| :class:`~pyTooling.CLIAbstraction.Argument.StringArgument`, :class:`~pyTooling.CLIAbstraction.Argument.PathArgument` * names and values |br| |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag`, :mod:`~pyTooling.CLIAbstraction.OptionalValuedFlag` * key-value pairs |br| |rarr| :mod:`~pyTooling.CLIAbstraction.NamedKeyValuePair` """ _pattern: ClassVar[str] def __init_subclass__(cls, *args: Any, pattern: Nullable[str] = None, **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``None``. :param kwargs: Any keyword argument. """ super().__init_subclass__(*args, **kwargs) cls._pattern = pattern # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is CommandLineArgument: raise TypeError(f"Class '{cls.__name__}' is abstract.") # TODO: not sure why parameters meant for __init__ do reach this level and distract __new__ from it's work return super().__new__(cls) # TODO: Add property to read pattern @abstractmethod def AsArgument(self) -> Union[str, Iterable[str]]: # type: ignore[empty-body] """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal name and value. :return: Formatted argument. :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass. """ raise NotImplementedError(f"Method 'AsArgument' is an abstract method and must be implemented by a subclass.") @abstractmethod def __str__(self) -> str: # type: ignore[empty-body] """ Return a string representation of this argument instance. :return: Argument formatted and enclosed in double quotes. :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass. """ raise NotImplementedError(f"Method '__str__' is an abstract method and must be implemented by a subclass.") @abstractmethod def __repr__(self) -> str: # type: ignore[empty-body] """ Return a string representation of this argument instance. .. note:: By default, this method is identical to :meth:`__str__`. :return: Argument formatted and enclosed in double quotes. :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass. """ raise NotImplementedError(f"Method '__repr__' is an abstract method and must be implemented by a subclass.") @export class ExecutableArgument(CommandLineArgument): """ Represents the executable. """ _executable: Path def __init__(self, executable: Path) -> None: """ Initializes a ExecutableArgument instance. :param executable: Path to the executable. :raises TypeError: If parameter 'executable' is not of type :class:`~pathlib.Path`. """ if not isinstance(executable, Path): ex = TypeError("Parameter 'executable' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(executable)}'.") raise ex self._executable = executable @property def Executable(self) -> Path: """ Get the internal path to the wrapped executable. :return: Internal path to the executable. """ return self._executable @Executable.setter def Executable(self, value: Path) -> None: """ Set the internal path to the wrapped executable. :param value: Value to path to the executable. :raises TypeError: If value is not of type :class:`~pathlib.Path`. """ if not isinstance(value, Path): ex = TypeError("Parameter 'value' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex self._executable = value def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal path to the wrapped executable. :return: Formatted argument. """ return f"{self._executable}" def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Argument formatted and enclosed in double quotes. """ return f"\"{self._executable}\"" __repr__ = __str__ @export class DelimiterArgument(CommandLineArgument, pattern="--"): """ Represents a delimiter symbol like ``--``. """ def __init_subclass__(cls, *args: Any, pattern: str = "--", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"--"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern. :return: Formatted argument. """ return self._pattern def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Argument formatted and enclosed in double quotes. """ return f"\"{self._pattern}\"" __repr__ = __str__ @export class NamedArgument(CommandLineArgument, pattern="{0}"): """ Base-class for all command line arguments with a name. """ _name: ClassVar[str] def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param name: Name of the CLI argument. :param pattern: This pattern is used to format an argument. |br| Default: ``"{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) cls._name = name # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is NamedArgument: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @readonly def Name(self) -> str: """ Get the internal name. :return: Internal name. """ return self._name def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal name. :return: Formatted argument. :raises ValueError: If internal name is None. """ if self._name is None: raise ValueError(f"Internal value '_name' is None.") return self._pattern.format(self._name) def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Argument formatted and enclosed in double quotes. """ return f"\"{self.AsArgument()}\"" __repr__ = __str__ @export class ValuedArgument(CommandLineArgument, Generic[ValueT], pattern="{0}"): """ Base-class for all command line arguments with a value. """ _value: ValueT def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) def __init__(self, value: ValueT) -> None: """ Initializes a ValuedArgument instance. :param value: Value to be stored internally. :raises TypeError: If parameter 'value' is None. """ if value is None: raise ValueError("Parameter 'value' is None.") self._value = value @property def Value(self) -> ValueT: """ Get the internal value. :return: Internal value. """ return self._value @Value.setter def Value(self, value: ValueT) -> None: """ Set the internal value. :param value: Value to set. :raises ValueError: If value to set is None. """ if value is None: raise ValueError(f"Value to set is None.") self._value = value def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal value. :return: Formatted argument. """ return self._pattern.format(self._value) def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Argument formatted and enclosed in double quotes. """ return f"\"{self.AsArgument()}\"" __repr__ = __str__ class NamedAndValuedArgument(NamedArgument, ValuedArgument, Generic[ValueT], pattern="{0}={1}"): """ Base-class for all command line arguments with a name and a value. """ def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param name: Name of the CLI argument. :param pattern: This pattern is used to format an argument. |br| Default: ``"{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) del kwargs["name"] del kwargs["pattern"] ValuedArgument.__init_subclass__(*args, **kwargs) def __init__(self, value: ValueT) -> None: ValuedArgument.__init__(self, value) def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal name and value. :return: Formatted argument. :raises ValueError: If internal name is None. """ if self._name is None: raise ValueError(f"Internal value '_name' is None.") return self._pattern.format(self._name, self._value) def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Argument formatted and enclosed in double quotes. """ return f"\"{self.AsArgument()}\"" __repr__ = __str__ class NamedTupledArgument(NamedArgument, ValuedArgument, Generic[ValueT], pattern="{0}"): """ Class and base-class for all TupleFlag classes, which represents an argument with separate value. A tuple argument is a command line argument followed by a separate value. Name and value are passed as two arguments to the executable. **Example: ** * `width 100`` """ _valuePattern: ClassVar[str] def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", valuePattern: str = "{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param name: Name of the CLI argument. :param pattern: This pattern is used to format the CLI argument name. |br| Default: ``"{0}"``. :param valuePattern: This pattern is used to format the value. |br| Default: ``"{0}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) cls._valuePattern = valuePattern # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is NamedTupledArgument: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) def __init__(self, value: ValueT) -> None: ValuedArgument.__init__(self, value) # TODO: Add property to read value pattern # @property # def ValuePattern(self) -> str: # if self._valuePattern is None: # raise ValueError(f"") # XXX: add message # # return self._valuePattern def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a sequence of string representations with proper escaping using the matching pattern based on the internal name and value. :return: Formatted argument as tuple of strings. :raises ValueError: If internal name is None. """ if self._name is None: raise ValueError(f"Internal value '_name' is None.") return ( self._pattern.format(self._name), self._valuePattern.format(self._value) ) def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Space separated sequence of arguments formatted and each enclosed in double quotes. """ return " ".join([f"\"{item}\"" for item in self.AsArgument()]) def __repr__(self) -> str: """ Return a string representation of this argument instance. :return: Comma separated sequence of arguments formatted and each enclosed in double quotes. """ return ", ".join([f"\"{item}\"" for item in self.AsArgument()]) @export class StringArgument(ValuedArgument, pattern="{0}"): """ Represents a simple string argument. A list of strings is available as :class:`~pyTooling.CLIAbstraction.Argument.StringListArgument`. """ def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) @export class StringListArgument(ValuedArgument): """ Represents a list of string argument (:class:`~pyTooling.CLIAbstraction.Argument.StringArgument`).""" def __init__(self, values: Iterable[str]) -> None: """ Initializes a StringListArgument instance. :param values: An iterable of str instances. :raises TypeError: If iterable parameter 'values' contains elements not of type :class:`str`. """ self._values = [] for value in values: if not isinstance(value, str): ex = TypeError(f"Parameter 'values' contains elements which are not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(values)}'.") raise ex self._values.append(value) @property def Value(self) -> List[str]: """ Get the internal list of str objects. :return: Reference to the internal list of str objects. """ return self._values @Value.setter def Value(self, value: Iterable[str]) -> None: """ Overwrite all elements in the internal list of str objects. .. note:: The list object is not replaced, but cleared and then reused by adding the given elements in the iterable. :param value: List of str objects to set. :raises TypeError: If value contains elements, which are not of type :class:`str`. """ self._values.clear() for value in value: if not isinstance(value, str): ex = TypeError(f"Value contains elements which are not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex self._values.append(value) def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal value. :return: Sequence of formatted arguments. """ return [f"{value}" for value in self._values] def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Space separated sequence of arguments formatted and each enclosed in double quotes. """ return " ".join([f"\"{value}\"" for value in self.AsArgument()]) def __repr__(self) -> str: """ Return a string representation of this argument instance. :return: Comma separated sequence of arguments formatted and each enclosed in double quotes. """ return ", ".join([f"\"{value}\"" for value in self.AsArgument()]) # TODO: Add option to class if path should be checked for existence @export class PathArgument(CommandLineArgument): """ Represents a single path argument. A list of paths is available as :class:`~pyTooling.CLIAbstraction.Argument.PathListArgument`. """ # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`. _path: Path def __init__(self, path: Path) -> None: """ Initializes a PathArgument instance. :param path: Path to a filesystem object. :raises TypeError: If parameter 'path' is not of type :class:`~pathlib.Path`. """ if not isinstance(path, Path): ex = TypeError("Parameter 'path' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") raise ex self._path = path @property def Value(self) -> Path: """ Get the internal path object. :return: Internal path object. """ return self._path @Value.setter def Value(self, value: Path) -> None: """ Set the internal path object. :param value: Value to set. :raises TypeError: If value is not of type :class:`~pathlib.Path`. """ if not isinstance(value, Path): ex = TypeError("Value is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex self._path = value def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal value. :return: Formatted argument. """ return f"{self._path}" def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Argument formatted and enclosed in double quotes. """ return f"\"{self._path}\"" __repr__ = __str__ @export class PathListArgument(CommandLineArgument): """ Represents a list of path arguments (:class:`~pyTooling.CLIAbstraction.Argument.PathArgument`). """ # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`. _paths: List[Path] def __init__(self, paths: Iterable[Path]) -> None: """ Initializes a PathListArgument instance. :param paths: An iterable os Path instances. :raises TypeError: If iterable parameter 'paths' contains elements not of type :class:`~pathlib.Path`. """ self._paths = [] for path in paths: if not isinstance(path, Path): ex = TypeError(f"Parameter 'paths' contains elements which are not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") raise ex self._paths.append(path) @property def Value(self) -> List[Path]: """ Get the internal list of path objects. :return: Reference to the internal list of path objects. """ return self._paths @Value.setter def Value(self, value: Iterable[Path]) -> None: """ Overwrite all elements in the internal list of path objects. .. note:: The list object is not replaced, but cleared and then reused by adding the given elements in the iterable. :param value: List of path objects to set. :raises TypeError: If value contains elements, which are not of type :class:`~pathlib.Path`. """ self._paths.clear() for path in value: if not isinstance(path, Path): ex = TypeError(f"Value contains elements which are not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") raise ex self._paths.append(path) def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal value. :return: Sequence of formatted arguments. """ return [f"{path}" for path in self._paths] def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Space separated sequence of arguments formatted and each enclosed in double quotes. """ return " ".join([f"\"{value}\"" for value in self.AsArgument()]) def __repr__(self) -> str: """ Return a string representation of this argument instance. :return: Comma separated sequence of arguments formatted and each enclosed in double quotes. """ return ", ".join([f"\"{value}\"" for value in self.AsArgument()]) pyTooling-8.11.0/pyTooling/CLIAbstraction/BooleanFlag.py000066400000000000000000000277231513317154500231150ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Boolean flags are arguments with a name and different pattern for a positive (``True``) and negative (``False``) value. .. seealso:: * For simple flags. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.Flag` * For flags with a value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag` * For flags that have an optional value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.NamedOptionalValuedFlag` """ from typing import ClassVar, Union, Iterable, Any, Optional as Nullable, Self try: from pyTooling.Decorators import export from pyTooling.CLIAbstraction.Argument import NamedArgument, ValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export from CLIAbstraction.Argument import NamedArgument, ValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex @export class BooleanFlag(NamedArgument, ValuedArgument): """ Class and base-class for all BooleanFlag classes, which represents a flag argument with different pattern for an enabled/positive (``True``) or disabled/negative (``False``) state. When deriving a subclass from an abstract BooleanFlag class, the parameters ``pattern`` and ``falsePattern`` are expected. **Example:** * True: ``with-checks`` * False: ``without-checks`` """ _falsePattern: ClassVar[str] def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "with-{0}", falsePattern: str = "without-{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument when the value is ``True``. |br| Default: ``"with-{0}"``. :param falsePattern: This pattern is used to format an argument when the value is ``False``. |br| Default: ``"without-{0}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) del kwargs["name"] del kwargs["pattern"] ValuedArgument.__init_subclass__(*args, **kwargs) cls._falsePattern = falsePattern # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is BooleanFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) def __init__(self, value: bool) -> None: """Initializes a BooleanFlag instance. :param value: Initial value set for this argument instance. """ ValuedArgument.__init__(self, value) def AsArgument(self) -> Union[str, Iterable[str]]: """Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal name and value. :return: Formatted argument. :raises ValueError: If internal name is None. """ if self._name is None: raise ValueError(f"Internal value '_name' is None.") pattern = self._pattern if self._value is True else self._falsePattern return pattern.format(self._name) @export class ShortBooleanFlag(BooleanFlag, pattern="-with-{0}", falsePattern="-without-{0}"): """Represents a :py:class:`BooleanFlag` with a single dash. **Example:** * True: ``-with-checks`` * False: ``-without-checks`` """ def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "-with-{0}", falsePattern: str = "-without-{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument when the value is ``True``. |br| Default: ``"-with-{0}"``. :param falsePattern: This pattern is used to format an argument when the value is ``False``. |br| Default: ``"-without-{0}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern kwargs["falsePattern"] = falsePattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ShortBooleanFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class LongBooleanFlag(BooleanFlag, pattern="--with-{0}", falsePattern="--without-{0}"): """Represents a :py:class:`BooleanFlag` with a double dash. **Example:** * True: ``--with-checks`` * False: ``--without-checks`` """ def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "--with-{0}", falsePattern: str = "--without-{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument when the value is ``True``. |br| Default: ``"--with-{0}"``. :param falsePattern: This pattern is used to format an argument when the value is ``False``. |br| Default: ``"--without-{0}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern kwargs["falsePattern"] = falsePattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongBooleanFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class WindowsBooleanFlag(BooleanFlag, pattern="/with-{0}", falsePattern="/without-{0}"): """Represents a :py:class:`BooleanFlag` with a slash. **Example:** * True: ``/with-checks`` * False: ``/without-checks`` """ def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "/with-{0}", falsePattern: str = "/without-{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument when the value is ``True``. |br| Default: ``"/with-{0}"``. :param falsePattern: This pattern is used to format an argument when the value is ``False``. |br| Default: ``"/without-{0}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern kwargs["falsePattern"] = falsePattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is WindowsBooleanFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) pyTooling-8.11.0/pyTooling/CLIAbstraction/Command.py000066400000000000000000000222641513317154500223150ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ This module implements command arguments. Usually, commands are mutually exclusive and the first argument in a list of arguments to a program. While commands can or cannot have prefix characters, they shouldn't be confused with flag arguments or string arguments. **Example:** * ``prog command -arg1 --argument2`` .. seealso:: * For simple flags (various formats). |br| |rarr| :mod:`~pyTooling.CLIAbstraction.Flag` * For string arguments. |br| |rarr| :class:`~pyTooling.CLIAbstraction.Argument.StringArgument` """ from typing import Any, Self try: from pyTooling.Decorators import export from pyTooling.CLIAbstraction.Argument import NamedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export from CLIAbstraction.Argument import NamedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex # TODO: make this class abstract @export class CommandArgument(NamedArgument): """ Represents a command argument. It is usually used to select a sub parser in a CLI argument parser or to hand over all following parameters to a separate tool. An example for a command is 'checkout' in ``git.exe checkout``, which calls ``git-checkout.exe``. **Example:** * ``command`` """ # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is CommandArgument: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class ShortCommand(CommandArgument, pattern="-{0}"): """ Represents a command name with a single dash. **Example:** * ``-command`` """ def __init_subclass__(cls, *args: Any, pattern: str = "-{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"-{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ShortCommand: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class LongCommand(CommandArgument, pattern="--{0}"): """ Represents a command name with a double dash. **Example:** * ``--command`` """ def __init_subclass__(cls, *args: Any, pattern: str = "--{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"--{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongCommand: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class WindowsCommand(CommandArgument, pattern="/{0}"): """ Represents a command name with a single slash. **Example:** * ``/command`` """ def __init_subclass__(cls, *args: Any, pattern: str = "/{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"/{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is WindowsCommand: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) pyTooling-8.11.0/pyTooling/CLIAbstraction/Flag.py000066400000000000000000000220601513317154500216020ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Flag arguments represent simple boolean values by being present or absent. .. seealso:: * For flags with different pattern based on the boolean value itself. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.BooleanFlag` * For flags with a value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag` * For flags that have an optional value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.NamedOptionalValuedFlag` """ from typing import Any, Self try: from pyTooling.Decorators import export from pyTooling.CLIAbstraction.Argument import NamedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export from CLIAbstraction.Argument import NamedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex @export class FlagArgument(NamedArgument): """ Base-class for all Flag classes, which represents a simple flag argument like ``-v`` or ``--verbose``. A simple flag is a single value (absent/present or off/on) with no additional data (value). """ # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is FlagArgument: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class ShortFlag(FlagArgument, pattern="-{0}"): """ Represents a :class:`~pyTooling.CLIAbstraction.Flag.Flag` argument with a single dash. **Example:** * ``-optimize`` """ def __init_subclass__(cls, *args: Any, pattern: str = "-{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"-{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ShortFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class LongFlag(FlagArgument, pattern="--{0}"): """ Represents a :class:`~pyTooling.CLIAbstraction.Flag.Flag` argument with a double dash. **Example:** * ``--optimize`` """ def __init_subclass__(cls, *args: Any, pattern: str = "--{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"--{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class WindowsFlag(FlagArgument, pattern="/{0}"): """ Represents a :class:`~pyTooling.CLIAbstraction.Flag.Flag` argument with a single slash. **Example:** * ``/optimize`` """ def __init_subclass__(cls, *args: Any, pattern: str = "/{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"/{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is WindowsFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) pyTooling-8.11.0/pyTooling/CLIAbstraction/KeyValueFlag.py000066400000000000000000000306721513317154500232600ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Flag arguments represent simple boolean values by being present or absent. .. seealso:: * For flags with different pattern based on the boolean value itself. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.BooleanFlag` * For flags with a value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag` * For flags that have an optional value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.NamedOptionalValuedFlag` """ from typing import Union, Iterable, Dict, cast, Any, Optional as Nullable, Self try: from pyTooling.Decorators import export from pyTooling.Common import getFullyQualifiedName from pyTooling.CLIAbstraction.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export from Common import getFullyQualifiedName from CLIAbstraction.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex @export class NamedKeyValuePairsArgument(NamedAndValuedArgument, pattern="{0}{1}={2}"): """ Class and base-class for all KeyValueFlag classes, which represents a flag argument with key and value (key-value-pairs). An optional valued flag is a flag name followed by a value. The default delimiter sign is equal (``=``). Name and value are passed as one argument to the executable even if the delimiter sign is a whitespace character. If the value is None, no delimiter sign and value is passed. **Example:** * ``-gWidth=100`` """ def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}{1}={2}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param name: Name of the CLI argument. :param pattern: This pattern is used to format an argument. |br| Default: ``"{0}{1}={2}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is NamedKeyValuePairsArgument: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) def __init__(self, keyValuePairs: Dict[str, str]) -> None: super().__init__({}) for key, value in keyValuePairs.items(): if not isinstance(key, str): ex = TypeError(f"Parameter 'keyValuePairs' contains a pair, where the key is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.") raise ex elif not isinstance(value, str): ex = TypeError(f"Parameter 'keyValuePairs' contains a pair, where the value is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex self._value[key] = value @property def Value(self) -> Dict[str, str]: """ Get the internal value. :return: Internal value. """ return self._value @Value.setter def Value(self, keyValuePairs: Dict[str, str]) -> None: """ Set the internal value. :param keyValuePairs: Value to set. :raises ValueError: If value to set is None. """ innerDict = cast(Dict[str, str], self._value) innerDict.clear() for key, value in keyValuePairs.items(): if not isinstance(key, str): ex = TypeError(f"Parameter 'keyValuePairs' contains a pair, where the key is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.") raise ex elif not isinstance(value, str): ex = TypeError(f"Parameter 'keyValuePairs' contains a pair, where the value is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex innerDict[key] = value def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal name. :return: Formatted argument. :raises ValueError: If internal name is None. """ if self._name is None: raise ValueError(f"Internal value '_name' is None.") return [self._pattern.format(self._name, key, value) for key, value in self._value.items()] @export class ShortKeyValueFlag(NamedKeyValuePairsArgument, pattern="-{0}{1}={2}"): """ Represents a :py:class:`NamedKeyValueFlagArgument` with a single dash in front of the switch name. **Example:** * ``-DDEBUG=TRUE`` """ def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "-{0}{1}={2}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param name: Name of the CLI argument. :param pattern: This pattern is used to format an argument. |br| Default: ``"-{0}{1}={2}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ShortKeyValueFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class LongKeyValueFlag(NamedKeyValuePairsArgument, pattern="--{0}{1}={2}"): """ Represents a :py:class:`NamedKeyValueFlagArgument` with a double dash in front of the switch name. **Example:** * ``--DDEBUG=TRUE`` """ def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "--{0}{1}={2}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param name: Name of the CLI argument. :param pattern: This pattern is used to format an argument. |br| Default: ``"--{0}{1}={2}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongKeyValueFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class WindowsKeyValueFlag(NamedKeyValuePairsArgument, pattern="/{0}:{1}={2}"): """ Represents a :py:class:`NamedKeyValueFlagArgument` with a double dash in front of the switch name. **Example:** * ``--DDEBUG=TRUE`` """ def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "/{0}:{1}={2}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param name: Name of the CLI argument. :param pattern: This pattern is used to format an argument. |br| Default: ``"/{0}:{1}={2}"``. :param kwargs: Any keyword argument. """ kwargs["name"] = name kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongKeyValueFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) pyTooling-8.11.0/pyTooling/CLIAbstraction/OptionalValuedFlag.py000066400000000000000000000270521513317154500244570ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ .. TODO:: Write module documentation. """ from typing import ClassVar, Union, Iterable, Any, Optional as Nullable, Self try: from pyTooling.Decorators import export from pyTooling.CLIAbstraction.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export from CLIAbstraction.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex @export class OptionalValuedFlag(NamedAndValuedArgument, pattern="{0"): """ Class and base-class for all OptionalValuedFlag classes, which represents a flag argument with data. An optional valued flag is a flag name followed by a value. The default delimiter sign is equal (``=``). Name and value are passed as one argument to the executable even if the delimiter sign is a whitespace character. If the value is None, no delimiter sign and value is passed. Example: ``width=100`` """ _patternWithValue: ClassVar[str] def __init_subclass__(cls, *args: Any, pattern: str = "{0}", patternWithValue: str = "{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument without a value. |br| Default: ``"{0}"``. :param patternWithValue: This pattern is used to format an argument with a value. |br| Default: ``"{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) cls._patternWithValue = patternWithValue # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is OptionalValuedFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) def __init__(self, value: Nullable[str] = None) -> None: self._value = value @property def Value(self) -> Nullable[str]: """ Get the internal value. :return: Internal value. """ return self._value @Value.setter def Value(self, value: Nullable[str]) -> None: """ Set the internal value. :param value: Value to set. """ self._value = value def AsArgument(self) -> Union[str, Iterable[str]]: """ Convert this argument instance to a string representation with proper escaping using the matching pattern based on the internal name and optional value. :return: Formatted argument. :raises ValueError: If internal name is None. """ if self._name is None: raise ValueError(f"Internal value '_name' is None.") pattern = self._pattern if self._value is None else self._patternWithValue return pattern.format(self._name, self._value) def __str__(self) -> str: return f"\"{self.AsArgument()}\"" __repr__ = __str__ @export class ShortOptionalValuedFlag(OptionalValuedFlag, pattern="-{0}", patternWithValue="-{0}={1}"): """ Represents a :py:class:`OptionalValuedFlag` with a single dash. Example: ``-optimizer=on`` """ def __init_subclass__(cls, *args: Any, pattern: str = "-{0}", patternWithValue: str = "-{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument without a value. |br| Default: ``"-{0}"``. :param patternWithValue: This pattern is used to format an argument with a value. |br| Default: ``"-{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern kwargs["patternWithValue"] = patternWithValue super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ShortOptionalValuedFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class LongOptionalValuedFlag(OptionalValuedFlag, pattern="--{0}", patternWithValue="--{0}={1}"): """ Represents a :py:class:`OptionalValuedFlag` with a double dash. Example: ``--optimizer=on`` """ def __init_subclass__(cls, *args: Any, pattern: str = "--{0}", patternWithValue: str = "--{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument without a value. |br| Default: ``"--{0}"``. :param patternWithValue: This pattern is used to format an argument with a value. |br| Default: ``"--{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern kwargs["patternWithValue"] = patternWithValue super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongOptionalValuedFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class WindowsOptionalValuedFlag(OptionalValuedFlag, pattern="/{0}", patternWithValue="/{0}:{1}"): """ Represents a :py:class:`OptionalValuedFlag` with a single slash. Example: ``/optimizer:on`` """ def __init_subclass__(cls, *args: Any, pattern: str = "/{0}", patternWithValue: str = "/{0}:{1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument without a value. |br| Default: ``"/{0}"``. :param patternWithValue: This pattern is used to format an argument with a value. |br| Default: ``"/{0}:{1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern kwargs["patternWithValue"] = patternWithValue super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is WindowsOptionalValuedFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) pyTooling-8.11.0/pyTooling/CLIAbstraction/ValuedFlag.py000066400000000000000000000235511513317154500227510ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Valued flags are arguments with a name and an always present value. The usual delimiter sign between name and value is an equal sign (``=``). .. seealso:: * For simple flags. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.Flag` * For flags with different pattern based on the boolean value itself. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.BooleanFlag` * For flags that have an optional value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.NamedOptionalValuedFlag` * For list of valued flags. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlagList` """ from typing import Any, Self try: from pyTooling.Decorators import export from pyTooling.CLIAbstraction.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export from CLIAbstraction.Argument import NamedAndValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex @export class ValuedFlag(NamedAndValuedArgument, pattern="{0}={1}"): """ Class and base-class for all ValuedFlag classes, which represents a flag argument with value. A valued flag is a flag name followed by a value. The default delimiter sign is equal (``=``). Name and value are passed as one argument to the executable even if the delimiter sign is a whitespace character. **Example:** * ``width=100`` """ def __init_subclass__(cls, *args: Any, pattern: str = "{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ValuedFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class ShortValuedFlag(ValuedFlag, pattern="-{0}={1}"): """ Represents a :py:class:`ValuedFlagArgument` with a single dash. **Example:** * ``-optimizer=on`` """ def __init_subclass__(cls, *args: Any, pattern: str = "-{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"-{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ShortValuedFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class LongValuedFlag(ValuedFlag, pattern="--{0}={1}"): """ Represents a :py:class:`ValuedFlagArgument` with a double dash. **Example:** * ``--optimizer=on`` """ def __init_subclass__(cls, *args: Any, pattern: str = "--{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"--{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongValuedFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class WindowsValuedFlag(ValuedFlag, pattern="/{0}:{1}"): """ Represents a :py:class:`ValuedFlagArgument` with a single slash. **Example:** * ``/optimizer:on`` """ # TODO: Is it possible to copy the doc-string from super? def __init_subclass__(cls, *args: Any, pattern: str = "/{0}:{1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"/{0}:{1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is WindowsValuedFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) pyTooling-8.11.0/pyTooling/CLIAbstraction/ValuedFlagList.py000066400000000000000000000267671513317154500236210ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ List of valued flags are argument lists where each item is a valued flag (See :mod:`~pyTooling.CLIAbstraction.ValuedFlag.ValuedFlag`). Each list item gets translated into a ``***ValuedFlag``, with the same flag name, but differing values. .. seealso:: * For single valued flags. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag` * For list of strings. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.Argument.StringListArgument` * For list of paths. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.Argument.PathListArgument` """ from typing import List, Union, Iterable, cast, Any, Self try: from pyTooling.Decorators import export from pyTooling.Common import getFullyQualifiedName from pyTooling.CLIAbstraction.Argument import ValueT, NamedAndValuedArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export from Common import getFullyQualifiedName from CLIAbstraction.Argument import ValueT, NamedAndValuedArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex @export class ValuedFlagList(NamedAndValuedArgument, pattern="{0}={1}"): """ Class and base-class for all ValuedFlagList classes, which represents a list of valued flags. Each list element gets translated to a valued flag using the pattern for formatting. See :mod:`~pyTooling.CLIAbstraction.ValuedFlag` for more details on valued flags. **Example:** * ``file=file1.log file=file2.log`` """ def __init_subclass__(cls, *args: Any, pattern: str = "{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ValuedFlagList: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) def __init__(self, value: List[ValueT]) -> None: super().__init__(list(value)) @property def Value(self) -> List[str]: """ Get the internal value. :return: Internal value. """ return self._value @Value.setter def Value(self, values: Iterable[str]) -> None: """ Set the internal value. :param values: List of values to set. :raises ValueError: If a list element is not o type :class:`str`. """ innerList = cast(list, self._value) innerList.clear() for value in values: if not isinstance(value, str): ex = TypeError(f"Value contains elements which are not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex innerList.append(value) def AsArgument(self) -> Union[str, Iterable[str]]: if self._name is None: raise ValueError(f"") # XXX: add message return [self._pattern.format(self._name, value) for value in self._value] def __str__(self) -> str: """ Return a string representation of this argument instance. :return: Space separated sequence of arguments formatted and each enclosed in double quotes. """ return " ".join([f"\"{value}\"" for value in self.AsArgument()]) def __repr__(self) -> str: """ Return a string representation of this argument instance. :return: Comma separated sequence of arguments formatted and each enclosed in double quotes. """ return ", ".join([f"\"{value}\"" for value in self.AsArgument()]) @export class ShortValuedFlagList(ValuedFlagList, pattern="-{0}={1}"): """ Represents a :py:class:`ValuedFlagArgument` with a single dash. **Example:** * ``-file=file1.log -file=file2.log`` """ def __init_subclass__(cls, *args: Any, pattern: str = "-{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"-{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ShortValuedFlagList: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class LongValuedFlagList(ValuedFlagList, pattern="--{0}={1}"): """ Represents a :py:class:`ValuedFlagArgument` with a double dash. **Example:** * ``--file=file1.log --file=file2.log`` """ def __init_subclass__(cls, *args: Any, pattern: str = "--{0}={1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"--{0}={1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongValuedFlagList: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class WindowsValuedFlagList(ValuedFlagList, pattern="/{0}:{1}"): """ Represents a :py:class:`ValuedFlagArgument` with a single slash. **Example:** * ``/file:file1.log /file:file2.log`` """ # TODO: Is it possible to copy the doc-string from super? def __init_subclass__(cls, *args: Any, pattern: str = "/{0}:{1}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"/{0}:{1}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is WindowsValuedFlagList: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) pyTooling-8.11.0/pyTooling/CLIAbstraction/ValuedTupleFlag.py000066400000000000000000000201711513317154500237560ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Valued tuple-flag arguments represent a name and a value as a 2-tuple. .. seealso:: * For flags with a value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag` * For flags that have an optional value. |br| |rarr| :mod:`~pyTooling.CLIAbstraction.NamedOptionalValuedFlag` """ from typing import Any, Self try: from pyTooling.Decorators import export from pyTooling.CLIAbstraction.Argument import NamedTupledArgument except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export from CLIAbstraction.Argument import NamedTupledArgument except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex @export class ShortTupleFlag(NamedTupledArgument, pattern="-{0}"): """ Represents a :class:`ValuedTupleArgument` with a single dash in front of the switch name. **Example:** * ``-file file1.txt`` """ def __init_subclass__(cls, *args: Any, pattern: str = "-{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"-{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is ShortTupleFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class LongTupleFlag(NamedTupledArgument, pattern="--{0}"): """ Represents a :class:`ValuedTupleArgument` with a double dash in front of the switch name. **Example:** * ``--file file1.txt`` """ def __init_subclass__(cls, *args: Any, pattern: str = "--{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"--{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is LongTupleFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) @export class WindowsTupleFlag(NamedTupledArgument, pattern="/{0}"): """ Represents a :class:`ValuedTupleArgument` with a single slash in front of the switch name. **Example:** * ``/file file1.txt`` """ def __init_subclass__(cls, *args: Any, pattern: str = "/{0}", **kwargs: Any) -> None: """ This method is called when a class is derived. :param args: Any positional arguments. :param pattern: This pattern is used to format an argument. |br| Default: ``"/{0}"``. :param kwargs: Any keyword argument. """ kwargs["pattern"] = pattern super().__init_subclass__(*args, **kwargs) # TODO: the whole class should be marked as abstract # TODO: a decorator should solve the issue and overwrite the __new__ method with that code def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ Check if this class was directly instantiated without being derived to a subclass. If so, raise an error. :param args: Any positional arguments. :param kwargs: Any keyword arguments. :raises TypeError: When this class gets directly instantiated without being derived to a subclass. """ if cls is WindowsTupleFlag: raise TypeError(f"Class '{cls.__name__}' is abstract.") return super().__new__(cls, *args, **kwargs) pyTooling-8.11.0/pyTooling/CLIAbstraction/__init__.py000066400000000000000000000623051513317154500224760ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Basic abstraction layer for executables.""" # __keywords__ = ["abstract", "executable", "cli", "cli arguments"] from os import environ as os_environ from pathlib import Path from platform import system from shutil import which as shutil_which from subprocess import Popen as Subprocess_Popen, PIPE as Subprocess_Pipe, STDOUT as Subprocess_StdOut, TimeoutExpired from typing import Dict, Optional as Nullable, ClassVar, Type, List, Tuple, Iterator, Generator, Any, Mapping, Iterable try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyTooling.Exceptions import ToolingException, PlatformNotSupportedException from pyTooling.Common import getFullyQualifiedName from pyTooling.Attributes import Attribute from pyTooling.CLIAbstraction.Argument import CommandLineArgument, ExecutableArgument from pyTooling.CLIAbstraction.Argument import NamedAndValuedArgument, ValuedArgument, PathArgument, PathListArgument, NamedTupledArgument from pyTooling.CLIAbstraction.ValuedFlag import ValuedFlag from pyTooling.Platform import Platform except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.CLIAbstraction] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType from Exceptions import ToolingException, PlatformNotSupportedException from Common import getFullyQualifiedName from Attributes import Attribute from CLIAbstraction.Argument import CommandLineArgument, ExecutableArgument from CLIAbstraction.Argument import NamedAndValuedArgument, ValuedArgument, PathArgument, PathListArgument, NamedTupledArgument from CLIAbstraction.ValuedFlag import ValuedFlag from Platform import Platform except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.CLIAbstraction] Could not import directly!") raise ex @export class CLIAbstractionException(ToolingException): """Base-exception of all exceptions raised by :mod:`pyTooling.CLIAbstraction`.""" @export class DryRunException(CLIAbstractionException): """This exception is raised if an executable is launched while in dry-run mode.""" @export class CLIArgument(Attribute): """An attribute to annotate nested classes as an CLI argument.""" @export class Environment(metaclass=ExtendedType, slots=True): """ A class describing the environment of an executable. .. topic:: Content of the environment * Environment variables """ _variables: Dict[str, str] #: Dictionary of active environment variables. # TODO: derive environment from existing environment object. def __init__( self, *, environment: Nullable["Environment"] = None, newVariables: Nullable[Mapping[str, str]] = None, addVariables: Nullable[Mapping[str, str]] = None, delVariables: Nullable[Iterable[str]] = None ) -> None: """ Initializes an environment class managing. .. topic:: Algorithm 1. Create a new dictionary of environment variables (name-value pairs) from either: * an existing :class:`Environment` instance. * current executable's environment by reading environment variables from :func:`os.environ`. * a dictionary of name-value pairs. 2. Remove variables from environment. 3. Add new or update existing variables. :param environment: Optional existing Environment instance to derive a new environment. :param newVariables: Optional dictionary of new environment variables. |br| If ``None``, read current environment variables from :func:`os.environ`. :param addVariables: Optional dictionary of variables to be added or modified in the environment. :param delVariables: Optional list of variable names to be removed from the environment. """ if environment is not None: newVariables = environment._variables elif newVariables is None: newVariables = os_environ self._variables = {name: value for name, value in newVariables.items()} if delVariables is not None: for variableName in delVariables: del self._variables[variableName] if addVariables is not None: self._variables.update(addVariables) def __len__(self) -> len: """ Returns the number of set environment variables. :returns: Number of environment variables. """ return len(self._variables) def __contains__(self, name: str) -> bool: """ Checks if the variable is set in the environment. :param key: The variable name to check. :returns: ``True``, if the variable is set in the environment. """ return name in self._variables def __getitem__(self, name: str) -> str: """ Access an environment variable in the environment by name. :param name: Name of the environment variable. :returns: The environment variable's value. :raises KeyError: If Variable name is not set in the environment. """ return self._variables[name] def __setitem__(self, name: str, value: str) -> None: """ Add or set an environment variable in the environment by name. :param name: Name of the environment variable. :param value: Value of the environment variable to be set. """ self._variables[name] = value def __delitem__(self, name: str) -> None: """ Remove an environment variable from the environment by name. :param name: The name of the environment variable to remove. :raises KeyError: If name doesn't exist in the environment. """ del self._variables[name] @export class Program(metaclass=ExtendedType, slots=True): """ Represent a simple command line interface (CLI) executable (program or script). CLI options are collected in a ``__cliOptions__`` dictionary. """ _platform: str #: Current platform the executable runs on (Linux, Windows, ...) _executableNames: ClassVar[Dict[str, str]] #: Dictionary of platform specific executable names. _executablePath: Path #: The path to the executable (binary, script, ...). _dryRun: bool #: True, if program shall run in *dry-run mode*. __cliOptions__: ClassVar[Dict[Type[CommandLineArgument], int]] #: List of all possible CLI options. __cliParameters__: Dict[Type[CommandLineArgument], Nullable[CommandLineArgument]] #: List of all CLI parameters (used CLI options). def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: """ Whenever a subclass is derived from :class:``Program``, all nested classes declared within ``Program`` and which are marked with attribute ``CLIArgument`` are collected and then listed in the ``__cliOptions__`` dictionary. :param args: Any positional arguments. :param kwargs: Any keyword arguments. """ super().__init_subclass__(*args, **kwargs) # register all available CLI options (nested classes marked with attribute 'CLIArgument') cls.__cliOptions__ = {option: order for order, option in enumerate(CLIArgument.GetClasses(scope=cls))} def __init__( self, executablePath: Nullable[Path] = None, binaryDirectoryPath: Nullable[Path] = None, dryRun: bool = False ) -> None: """ Initializes a program instance. .. todo:: Document algorithm :param executablePath: Path to the executable. :param binaryDirectoryPath: Path to the executable's directory. :param dryRun: True, when the program should run in dryrun mode. """ self._platform = system() self._dryRun = dryRun if executablePath is not None: if isinstance(executablePath, Path): if not executablePath.exists(): if dryRun: self.LogDryRun(f"File check for '{executablePath}' failed. [SKIPPING]") else: raise CLIAbstractionException(f"Program '{executablePath}' not found.") from FileNotFoundError(executablePath) else: ex = TypeError(f"Parameter 'executablePath' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(executablePath)}'.") raise ex elif binaryDirectoryPath is not None: if isinstance(binaryDirectoryPath, Path): if not binaryDirectoryPath.exists(): if dryRun: self.LogDryRun(f"Directory check for '{binaryDirectoryPath}' failed. [SKIPPING]") else: raise CLIAbstractionException(f"Binary directory '{binaryDirectoryPath}' not found.") from FileNotFoundError(binaryDirectoryPath) try: executablePath = binaryDirectoryPath / self.__class__._executableNames[self._platform] except KeyError: raise CLIAbstractionException(f"Program is not supported on platform '{self._platform}'.") from PlatformNotSupportedException(self._platform) if not executablePath.exists(): if dryRun: self.LogDryRun(f"File check for '{executablePath}' failed. [SKIPPING]") else: raise CLIAbstractionException(f"Program '{executablePath}' not found.") from FileNotFoundError(executablePath) else: ex = TypeError(f"Parameter 'binaryDirectoryPath' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(binaryDirectoryPath)}'.") raise ex else: try: executablePath = Path(self._executableNames[self._platform]) except KeyError: raise CLIAbstractionException(f"Program is not supported on platform '{self._platform}'.") from PlatformNotSupportedException(self._platform) resolvedExecutable = shutil_which(str(executablePath)) if dryRun: if resolvedExecutable is None: pass # XXX: log executable not found in PATH # self.LogDryRun(f"Which '{executablePath}' failed. [SKIPPING]") else: fullExecutablePath = Path(resolvedExecutable) if not fullExecutablePath.exists(): pass # XXX: log executable not found # self.LogDryRun(f"File check for '{fullExecutablePath}' failed. [SKIPPING]") else: if resolvedExecutable is None: raise CLIAbstractionException(f"Program could not be found in PATH.") from FileNotFoundError(executablePath) fullExecutablePath = Path(resolvedExecutable) if not fullExecutablePath.exists(): raise CLIAbstractionException(f"Program '{fullExecutablePath}' not found.") from FileNotFoundError(fullExecutablePath) # TODO: log found executable in PATH # TODO: check if found executable has execute permissions # raise ValueError(f"Neither parameter 'executablePath' nor 'binaryDirectoryPath' was set.") self._executablePath = executablePath self.__cliParameters__ = {} @staticmethod def _NeedsParameterInitialization(key) -> bool: return issubclass(key, (ValuedFlag, ValuedArgument, NamedAndValuedArgument, NamedTupledArgument, PathArgument, PathListArgument)) def __getitem__(self, key: Type[CommandLineArgument]) -> CommandLineArgument: """Access to a CLI parameter by CLI option (key must be of type :class:`CommandLineArgument`), which is already used.""" if not issubclass(key, CommandLineArgument): ex = TypeError(f"Key '{key}' is not a subclass of 'CommandLineArgument'.") ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.") raise ex # TODO: is nested check return self.__cliParameters__[key] def __setitem__(self, key: Type[CommandLineArgument], value: CommandLineArgument) -> None: if not issubclass(key, CommandLineArgument): ex = TypeError(f"Key '{key}' is not a subclass of 'CommandLineArgument'.") ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.") raise ex elif key not in self.__cliOptions__: raise KeyError(f"Option '{key}' is not allowed on executable '{self.__class__.__name__}'") elif key in self.__cliParameters__: raise KeyError(f"Option '{key}' is already set to a value.") if self._NeedsParameterInitialization(key): self.__cliParameters__[key] = key(value) else: self.__cliParameters__[key] = key() @readonly def Path(self) -> Path: """ Read-only property to access the program's path. :returns: The program's path. """ return self._executablePath def ToArgumentList(self) -> List[str]: """ Convert a program and used CLI options to a list of CLI argument strings in correct order and with escaping. :returns: List of CLI arguments """ result: List[str] = [] result.append(str(self._executablePath)) def predicate(item: Tuple[Type[CommandLineArgument], int]) -> int: return self.__cliOptions__[item[0]] for key, value in sorted(self.__cliParameters__.items(), key=predicate): param = value.AsArgument() if isinstance(param, str): result.append(param) elif isinstance(param, (Tuple, List)): result += param else: raise TypeError(f"") # XXX: needs error message return result def __repr__(self) -> str: """ Returns the string representation as coma-separated list of double-quoted CLI argument strings within square brackets. Example: :pycode:`["arg1", "arg2"]` :returns: Coma-separated list of CLI arguments with double-quotes. """ return "[" + ", ".join([f"\"{item}\"" for item in self.ToArgumentList()]) + "]" # WORKAROUND: Python <3.12 # return f"[{", ".join([f"\"{item}\"" for item in self.ToArgumentList()])}]" def __str__(self) -> str: """ Returns the string representation as space-separated list of double-quoted CLI argument strings. Example: :pycode:`"arg1" "arg2"` :returns: Space-separated list of CLI arguments with double-quotes. """ return " ".join([f"\"{item}\"" for item in self.ToArgumentList()]) @export class Executable(Program): # (ILogable): """Represent a CLI executable derived from :class:`Program`, that adds an abstraction of :class:`subprocess.Popen`.""" _BOUNDARY: ClassVar[str] = "====== BOUNDARY pyTooling.CLIAbstraction BOUNDARY ======" _workingDirectory: Nullable[Path] #: Path to the working directory _environment: Nullable[Environment] #: Environment to use when executing. _process: Nullable[Subprocess_Popen] #: Reference to the running process. _exitCode: Nullable[int] #: The child's process exit code. _killed: Nullable[bool] #: True, if the child-process got killed (e.g. by a timeout). _iterator: Nullable[Iterator] #: Iterator for reading STDOUT. def __init__( self, executablePath: Nullable[Path] = None, binaryDirectoryPath: Nullable[Path] = None, workingDirectory: Nullable[Path] = None, environment: Nullable[Environment] = None, dryRun: bool = False ) -> None: """ Initializes an executable instance. :param executablePath: Path to the executable. :param binaryDirectoryPath: Path to the executable's directory. :param workingDirectory: Path to the working directory. :param environment: Optional environment that should be setup when launching the executable. :param dryRun: True, when the program should run in dryrun mode. """ super().__init__(executablePath, binaryDirectoryPath, dryRun) self._workingDirectory = None self._environment = environment self._process = None self._exitCode = None self._killed = None self._iterator = None def StartProcess(self, environment: Nullable[Environment] = None) -> None: """ Start the executable as a child-process. :param environment: Optional environment that should be setup when launching the executable. |br| If ``None``, the :attr:`_environment` is used. :raises CLIAbstractionException: When an :exc:`OSError` occurs while launching the child-process. """ if self._dryRun: self.LogDryRun(f"Start process: {self!r}") return if environment is not None: envVariables = environment._variables elif self._environment is not None: envVariables = self._environment._variables else: envVariables = None # FIXME: verbose log start process # FIXME: debug log - parameter list try: self._process = Subprocess_Popen( self.ToArgumentList(), stdin=Subprocess_Pipe, stdout=Subprocess_Pipe, stderr=Subprocess_StdOut, cwd=self._workingDirectory, env=envVariables, universal_newlines=True, bufsize=256 ) except OSError as ex: raise CLIAbstractionException(f"Error while launching a process for '{self._executablePath}'.") from ex def Send(self, line: str, end: str = "\n") -> None: """ Send a string to STDIN of the running child-process. :param line: Line to send. :param end: Line end character. :raises CLIAbstractionException: When any error occurs while sending data to the child-process. """ try: self._process.stdin.write(line + end) self._process.stdin.flush() except Exception as ex: raise CLIAbstractionException(f"") from ex # XXX: need error message # This is TCL specific ... # def SendBoundary(self): # self.Send("puts \"{0}\"".format(self._pyIPCMI_BOUNDARY)) def GetLineReader(self) -> Generator[str, None, None]: """ Return a line-reader for STDOUT. :returns: A generator object to read from STDOUT line-by-line. :raises DryRunException: In case dryrun mode is active. :raises CLIAbstractionException: When any error occurs while reading outputs from the child-process. """ if self._dryRun: raise DryRunException() # XXX: needs a message try: for line in iter(self._process.stdout.readline, ""): # FIXME: can it be improved? yield line[:-1] except Exception as ex: raise CLIAbstractionException() from ex # XXX: need error message # finally: # self._process.terminate() def Wait(self, timeout: Nullable[float] = None, kill: bool = False) -> Nullable[int]: """ Wait on the child-process with an optional timeout. When the timeout period exceeds, the child-process can be forcefully terminated. :param timeout: Optional, timeout in seconds. |br| Default: infinitely wait on the child-process. :param kill: If true, terminate (kill) the child-process if it didn't terminate by itself within the timeout period. :returns: ``None`` when the child-process is still running, otherwise the exit code. :raises CLIAbstractionException: When the child-process is not started yet. .. topic:: Usecases :pycode:`executable.Wait()` Infinitely wait on the child-process. When the child-process terminates by itself, the exit code is returned. This is a blocking call. :pycode:`executable.Wait(timeout=5.4)` Wait for a specified time on the child-process' termination. If it terminated by itself within the specified timeout period, the exit code is returned; otherwise ``None``. Thus :pycode:`.Wait(timeout=0.0)` returning ``None`` indicates a running process. :pycode:`executable.Wait(timeout=20.0, kill=True)` Wait for a specified time on the child-process' termination. If it terminated by itself within the specified timeout period, the exit code is returned; otherwise the child-process gets killed and it's exit code is returned. :pycode:`executable.Wait(timeout=0.0, kill=True)` Kill immediately. .. seealso:: :meth:`Terminate` - Terminate the child-process. """ if self._process is None: raise CLIAbstractionException(f"Process not yet started.") try: self._exitCode = self._process.wait(timeout=timeout) except TimeoutExpired: # when timed out, the process isn't terminated/killed automatically if kill: self._killed = True self._process.terminate() # After killing, wait to clean up the "zombie" process self._exitCode = self._process.wait() return self._exitCode def Terminate(self) -> Nullable[int]: """ Terminate the child-process. :returns: The child-process' exit code. :raises CLIAbstractionException: When the child-process is not started yet. .. seealso:: :meth:`Wait` - Wait on the child-process with an optional timeout. """ return self.Wait(timeout=0.0, kill=True) @readonly def ExitCode(self) -> int: """ Read-only property accessing the child-process' exit code. :returns: Child-process' exit code or ``None`` if it's still running. .. seealso:: * :meth:`Wait` - Wait on the child-process with an optional timeout. * :meth:`Terminate` - Terminate the child-process. """ return self._exitCode # This is TCL specific # def ReadUntilBoundary(self, indent=0): # __indent = " " * indent # if self._iterator is None: # self._iterator = iter(self.GetReader()) # # for line in self._iterator: # print(__indent + line) # if self._pyIPCMI_BOUNDARY in line: # break # self.LogDebug("Quartus II is ready") @export class OutputFilteredExecutable(Executable): """Represent a CLI executable derived from :class:`Executable`, whose outputs are filtered.""" _hasOutput: bool _hasWarnings: bool _hasErrors: bool _hasFatals: bool def __init__(self, platform: Platform, dryrun: bool, executablePath: Path) -> None: #, environment=None, logger=None) -> None: super().__init__(platform, dryrun, executablePath) #, environment=environment, logger=logger) self._hasOutput = False self._hasWarnings = False self._hasErrors = False self._hasFatals = False @readonly def HasWarnings(self) -> bool: # TODO: update doc-string """True if warnings were found while processing the output stream.""" return self._hasWarnings @readonly def HasErrors(self) -> bool: # TODO: update doc-string """True if errors were found while processing the output stream.""" return self._hasErrors @readonly def HasFatals(self) -> bool: # TODO: update doc-string """True if fatals were found while processing the output stream.""" return self._hasErrors pyTooling-8.11.0/pyTooling/CallByRef/000077500000000000000000000000001513317154500173615ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/CallByRef/__init__.py000066400000000000000000000522501513317154500214760ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ ____ ____ __ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _| | | __ ) _ _| _ \ ___ / _| # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | | | _ \| | | | |_) / _ \ |_ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | |_) | |_| | _ < __/ _| # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_|_|____/ \__, |_| \_\___|_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Auxiliary classes to implement call-by-reference. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from decimal import Decimal from typing import Any, Generic, TypeVar, Optional as Nullable try: from pyTooling.Decorators import export from pyTooling.MetaClasses import ExtendedType except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.CallByRef] Could not import from 'pyTooling.*'!") try: from Decorators import export from MetaClasses import ExtendedType except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.CallByRef] Could not import directly!") raise ex T = TypeVar("T") @export class CallByRefParam(Generic[T], metaclass=ExtendedType, slots=True): """ Implements a *call-by-reference* parameter. .. seealso:: * :class:`CallByRefBoolParam` |br| |rarr| A special *call-by-reference* implementation for boolean reference types. * :class:`CallByRefIntParam` |br| |rarr| A special *call-by-reference* implementation for integer reference types. """ Value: T #: internal value def __init__(self, value: Nullable[T] = None) -> None: """Constructs a *call-by-reference* object for any type. :param value: The value to be set as an initial value. """ self.Value = value def __ilshift__(self, other: T) -> 'CallByRefParam[T]': # Starting with Python 3.11+, use typing.Self as return type """Assigns a value to the *call-by-reference* object. :param other: The value to be assigned to this *call-by-reference* object. :returns: Itself. """ self.Value = other return self # binary operators - comparison def __eq__(self, other: Any) -> bool: """ Compare a CallByRefParam wrapped value with another instances (CallbyRefParam) or non-wrapped value for equality. :param other: Parameter to compare against. :returns: ``True``, if both values are equal. """ if isinstance(other, CallByRefParam): return self.Value == other.Value else: return self.Value == other def __ne__(self, other) -> bool: """ Compare a CallByRefParam wrapped value with another instances (CallbyRefParam) or non-wrapped value for inequality. :param other: Parameter to compare against. :returns: ``True``, if both values are unequal. """ if isinstance(other, CallByRefParam): return self.Value != other.Value else: return self.Value != other # Type conversion operators def __repr__(self) -> str: """ Returns the wrapped object's string representation. :returns: The string representation of the wrapped value. """ return repr(self.Value) def __str__(self) -> str: """ Returns the wrapped object's string equivalent. :returns: The string equivalent of the wrapped value. """ return str(self.Value) @export class CallByRefBoolParam(CallByRefParam): """A special *call-by-reference* implementation for boolean reference types.""" # Binary operators - comparison def __eq__(self, other: Any) -> bool: """ Compare a CallByRefBoolParam wrapped boolean value with another instances (CallByRefBoolParam) or non-wrapped boolean value for equality. :param other: Parameter to compare against. :returns: ``True``, if both values are equal. :raises TypeError: If parameter ``other`` is not of type :class:`bool` or :class:`CallByRefBoolParam`. """ if isinstance(other, bool): return self.Value == other elif isinstance(other, CallByRefBoolParam): return self.Value == other.Value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.") ex.add_note("Supported types for second operand: bool, CallByRefBoolParam") raise ex def __ne__(self, other) -> bool: """ Compare a CallByRefBoolParam wrapped boolean value with another instances (CallByRefBoolParam) or non-wrapped boolean value for inequality. :param other: Parameter to compare against. :returns: ``True``, if both values are unequal. :raises TypeError: If parameter ``other`` is not of type :class:`bool` or :class:`CallByRefBoolParam`. """ if isinstance(other, bool): return self.Value != other elif isinstance(other, CallByRefBoolParam): return self.Value != other.Value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by != operator.") ex.add_note(f"Supported types for second operand: bool, CallByRefBoolParam") raise ex # Type conversion operators def __bool__(self) -> bool: """ Type conversion to :class:`bool`. :returns: The wrapped value. """ return self.Value def __int__(self) -> int: """ Type conversion to :class:`int`. :returns: The integer representation of the wrapped boolean value. """ return int(self.Value) @export class CallByRefIntParam(CallByRefParam): """A special *call-by-reference* implementation for integer reference types.""" # Unary operators def __neg__(self) -> int: """Negate: -self.""" return -self.Value def __pos__(self) -> int: """Positive: +self.""" return +self.Value def __invert__(self) -> int: """Invert: ~self.""" return ~self.Value # Binary operators - logical def __and__(self, other: Any) -> int: """And: self & other.""" if isinstance(other, int): return self.Value & other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by and operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __or__(self, other: Any) -> int: """Or: self | other.""" if isinstance(other, int): return self.Value | other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by or operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __xor__(self, other: Any) -> int: """Xor: self ^ other.""" if isinstance(other, int): return self.Value ^ other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by xor operator.") ex.add_note(f"Supported types for second operand: int") raise ex # Binary inplace operators def __iand__(self, other: Any) -> 'CallByRefIntParam': # Starting with Python 3.11+, use typing.Self as return type """Inplace and: self &= other.""" if isinstance(other, int): self.Value &= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by &= operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __ior__(self, other: Any) -> 'CallByRefIntParam': # Starting with Python 3.11+, use typing.Self as return type r"""Inplace or: self \|= other.""" if isinstance(other, int): self.Value |= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by |= operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __ixor__(self, other: Any) -> 'CallByRefIntParam': # Starting with Python 3.11+, use typing.Self as return type r"""Inplace or: self \|= other.""" if isinstance(other, int): self.Value ^= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by ^= operator.") ex.add_note(f"Supported types for second operand: int") raise ex # Binary operators - arithmetic def __add__(self, other: Any) -> int: """Addition: self + other.""" if isinstance(other, int): return self.Value + other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by + operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __sub__(self, other: Any) -> int: """Subtraction: self - other.""" if isinstance(other, int): return self.Value - other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by - operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __truediv__(self, other: Any) -> int: """Division: self / other.""" if isinstance(other, int): return self.Value / other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by / operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __floordiv__(self, other: Any) -> int: """Floor division: self // other.""" if isinstance(other, int): return self.Value // other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by // operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __mul__(self, other: Any) -> int: """Multiplication: self * other.""" if isinstance(other, int): return self.Value * other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by * operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __mod__(self, other: Any) -> int: """Modulo: self % other.""" if isinstance(other, int): return self.Value % other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by % operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __pow__(self, other: Any) -> int: """Power: self ** other.""" if isinstance(other, int): return self.Value ** other else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by ** operator.") ex.add_note(f"Supported types for second operand: int") raise ex # Binary inplace operators - arithmetic def __iadd__(self, other: Any) -> 'CallByRefIntParam': """Addition: self += other.""" if isinstance(other, int): self.Value += other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by xor operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __isub__(self, other: Any) -> 'CallByRefIntParam': """Subtraction: self -= other.""" if isinstance(other, int): self.Value -= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by xor operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __idiv__(self, other: Any) -> 'CallByRefIntParam': """Division: self /= other.""" if isinstance(other, int): self.Value /= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by xor operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __ifloordiv__(self, other: Any) -> 'CallByRefIntParam': """Floor division: self // other.""" if isinstance(other, int): self.Value //= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by xor operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __imul__(self, other: Any) -> 'CallByRefIntParam': r"""Multiplication: self \*= other.""" if isinstance(other, int): self.Value *= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by xor operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __imod__(self, other: Any) -> 'CallByRefIntParam': """Modulo: self %= other.""" if isinstance(other, int): self.Value %= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by xor operator.") ex.add_note(f"Supported types for second operand: int") raise ex def __ipow__(self, other: Any) -> 'CallByRefIntParam': r"""Power: self \*\*= other.""" if isinstance(other, int): self.Value **= other return self else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by xor operator.") ex.add_note(f"Supported types for second operand: int") raise ex # Binary operators - comparison def __eq__(self, other: Any) -> bool: """ Compare a CallByRefIntParam wrapped integer value with another instances (CallByRefIntParam) or non-wrapped integer value for equality. :param other: Parameter to compare against. :returns: ``True``, if both values are equal. :raises TypeError: If parameter ``other`` is not of type :class:`int`, :class:`float`, :class:`complex`, :class:`Decimal` or :class:`CallByRefParam`. """ if isinstance(other, (int, float, complex, Decimal)) and not isinstance(other, bool): return self.Value == other elif isinstance(other, CallByRefIntParam): return self.Value == other.Value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.") ex.add_note(f"Supported types for second operand: int, float, complex, Decimal, CallByRefIntParam") raise ex def __ne__(self, other) -> bool: """ Compare a CallByRefIntParam wrapped integer value with another instances (CallByRefIntParam) or non-wrapped integer value for inequality. :param other: Parameter to compare against. :returns: ``True``, if both values are unequal. :raises TypeError: If parameter ``other`` is not of type :class:`int`, :class:`float`, :class:`complex`, :class:`Decimal` or :class:`CallByRefParam`. """ if isinstance(other, (int, float, complex, Decimal)) and not isinstance(other, bool): return self.Value != other elif isinstance(other, CallByRefIntParam): return self.Value != other.Value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by != operator.") ex.add_note(f"Supported types for second operand: int, float, complex, Decimal, CallByRefIntParam") raise ex def __lt__(self, other: Any) -> bool: """ Compare a CallByRefIntParam wrapped integer value with another instances (CallByRefIntParam) or non-wrapped integer value for less-than. :param other: Parameter to compare against. :returns: ``True``, if the wrapped value is less than the other value. :raises TypeError: If parameter ``other`` is not of type :class:`int`, :class:`float`, :class:`complex`, :class:`Decimal` or :class:`CallByRefParam`. """ if isinstance(other, (int, float, complex, Decimal)) and not isinstance(other, bool): return self.Value < other elif isinstance(other, CallByRefIntParam): return self.Value < other.Value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.") ex.add_note(f"Supported types for second operand: int, float, complex, Decimal, CallByRefIntParam") raise ex def __le__(self, other: Any) -> bool: """ Compare a CallByRefIntParam wrapped integer value with another instances (CallByRefIntParam) or non-wrapped integer value for less-than-or-equal. :param other: Parameter to compare against. :returns: ``True``, if the wrapped value is less than or equal the other value. :raises TypeError: If parameter ``other`` is not of type :class:`int`, :class:`float`, :class:`complex`, :class:`Decimal` or :class:`CallByRefParam`. """ if isinstance(other, (int, float, complex, Decimal)) and not isinstance(other, bool): return self.Value <= other elif isinstance(other, CallByRefIntParam): return self.Value <= other.Value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.") ex.add_note(f"Supported types for second operand: int, float, complex, Decimal, CallByRefIntParam") raise ex def __gt__(self, other: Any) -> bool: """ Compare a CallByRefIntParam wrapped integer value with another instances (CallByRefIntParam) or non-wrapped integer value for geater-than. :param other: Parameter to compare against. :returns: ``True``, if the wrapped value is greater than the other value. :raises TypeError: If parameter ``other`` is not of type :class:`int`, :class:`float`, :class:`complex`, :class:`Decimal` or :class:`CallByRefParam`. """ if isinstance(other, (int, float, complex, Decimal)) and not isinstance(other, bool): return self.Value > other elif isinstance(other, CallByRefIntParam): return self.Value > other.Value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.") ex.add_note(f"Supported types for second operand: int, float, complex, Decimal, CallByRefIntParam") raise ex def __ge__(self, other: Any) -> bool: """ Compare a CallByRefIntParam wrapped integer value with another instances (CallByRefIntParam) or non-wrapped integer value for greater-than-or-equal. :param other: Parameter to compare against. :returns: ``True``, if the wrapped value is greater than or equal the other value. :raises TypeError: If parameter ``other`` is not of type :class:`int`, :class:`float`, :class:`complex`, :class:`Decimal` or :class:`CallByRefParam`. """ if isinstance(other, (int, float, complex, Decimal)) and not isinstance(other, bool): return self.Value >= other elif isinstance(other, CallByRefIntParam): return self.Value >= other.Value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.") ex.add_note(f"Supported types for second operand: int, float, complex, Decimal, CallByRefIntParam") raise ex # Type conversion operators def __bool__(self) -> bool: """ Type conversion to :class:`bool`. :returns: The boolean representation of the wrapped integer value. """ return bool(self.Value) def __int__(self) -> int: """ Type conversion to :class:`int`. :returns: The wrapped value.""" return self.Value def __float__(self) -> float: """ Type conversion to :class:`float`. :returns: The float representation of the wrapped integer value. """ return float(self.Value) pyTooling-8.11.0/pyTooling/Cartesian2D/000077500000000000000000000000001513317154500176555ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Cartesian2D/Shapes.py000066400000000000000000000172471513317154500214650ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ ____ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ \| _ \ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ __) | | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |/ __/| |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|_____|____/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """An implementation of 2D cartesian shapes for Python.""" from typing import Generic, Tuple, Optional as Nullable try: from pyTooling.Decorators import readonly, export from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType from pyTooling.Common import getFullyQualifiedName from pyTooling.Cartesian2D import Coordinate, Point2D, LineSegment2D except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!") try: from Decorators import readonly, export from Exceptions import ToolingException from MetaClasses import ExtendedType from Common import getFullyQualifiedName from Cartesian2D import Coordinate, Point2D, LineSegment2D except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Cartesian2D] Could not import directly!") raise ex @export class Shape(Generic[Coordinate]): """Base-class for all 2D cartesian shapes.""" @export class Trapezium(Shape[Coordinate], Generic[Coordinate]): """ A Trapezium is a four-sided polygon, having four edges (sides) and four corners (vertices). """ points: Tuple[Point2D[Coordinate], ...] #: A tuple of 2D-points describing the trapezium. segments: Tuple[LineSegment2D[Coordinate], ...] #: A tuple of 2D line segments describing the trapezium. def __init__(self, p00: Point2D[Coordinate], p01: Point2D[Coordinate], p11: Point2D[Coordinate], p10: Point2D[Coordinate]) -> None: """ Initializes a trapezium with 4 corners. :param p00: First corner. :param p01: Second corner. :param p11: Third corner. :param p10: Forth corner """ if not isinstance(p00, Point2D): ex = TypeError(f"Parameter 'p00' is not of type Point2D.") ex.add_note(f"Got type '{getFullyQualifiedName(p00)}'.") raise ex if not isinstance(p01, Point2D): ex = TypeError(f"Parameter 'p01' is not of type Point2D.") ex.add_note(f"Got type '{getFullyQualifiedName(p01)}'.") raise ex if not isinstance(p11, Point2D): ex = TypeError(f"Parameter 'p11' is not of type Point2D.") ex.add_note(f"Got type '{getFullyQualifiedName(p11)}'.") raise ex if not isinstance(p10, Point2D): ex = TypeError(f"Parameter 'p10' is not of type Point2D.") ex.add_note(f"Got type '{getFullyQualifiedName(p10)}'.") raise ex self.points = ( _p00 := p00.Copy(), _p01 := p01.Copy(), _p11 := p11.Copy(), _p10 := p10.Copy(), ) self.segments = ( LineSegment2D(_p00, _p01, copyPoints=False), LineSegment2D(_p01, _p11, copyPoints=False), LineSegment2D(_p11, _p10, copyPoints=False), LineSegment2D(_p10, _p00, copyPoints=False) ) @export class Rectangle(Trapezium[Coordinate]): """ A rectangle is a trapezium, where opposite edges a parallel to each other and all inner angels are 90 |degree| . """ def __init__(self, p00: Point2D[Coordinate], p01: Point2D[Coordinate], p11: Point2D[Coordinate], p10: Point2D[Coordinate]) -> None: """ Initializes a rectangle with 4 corners. :param p00: First corner. :param p01: Second corner. :param p11: Third corner. :param p10: Forth corner """ super().__init__(p00, p01, p11, p10) if self.segments[0].Length != self.segments[2].Length or self.segments[1].Length != self.segments[3].Length: raise ValueError(f"Line segments (edges) of opposite edges different lengths.") if (self.segments[0].AngleTo(self.segments[1]) == 0.0 and self.segments[1].AngleTo(self.segments[2]) == 0.0 and self.segments[2].AngleTo(self.segments[3]) == 0.0 and self.segments[3].AngleTo(self.segments[0]) == 0.0): raise ValueError(f"Line segments (edges) have no 90° angles.") @export class Square(Rectangle[Coordinate]): """ A square is a rectangle, where all edges have the same length and all inner angels are 90 |degree| . """ def __init__(self, p00: Point2D[Coordinate], p01: Point2D[Coordinate], p11: Point2D[Coordinate], p10: Point2D[Coordinate]) -> None: """ Initializes a square with 4 corners. :param p00: First corner. :param p01: Second corner. :param p11: Third corner. :param p10: Forth corner """ super().__init__(p00, p01, p11, p10) if self.segments[0].Length != self.segments[1].Length: raise ValueError(f"Line segments (edges) between corners have different lengths.") pyTooling-8.11.0/pyTooling/Cartesian2D/__init__.py000066400000000000000000000467721513317154500220060ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ ____ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ \| _ \ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ __) | | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |/ __/| |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|_____|____/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """An implementation of 2D cartesian data structures for Python.""" from math import sqrt, acos from typing import TypeVar, Union, Generic, Any, Tuple try: from pyTooling.Decorators import readonly, export from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType from pyTooling.Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!") try: from Decorators import readonly, export from Exceptions import ToolingException from MetaClasses import ExtendedType from Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Cartesian2D] Could not import directly!") raise ex Coordinate = TypeVar("Coordinate", bound=Union[int, float]) @export class Point2D(Generic[Coordinate], metaclass=ExtendedType, slots=True): """An implementation of a 2D cartesian point.""" x: Coordinate #: The x-direction coordinate. y: Coordinate #: The y-direction coordinate. def __init__(self, x: Coordinate, y: Coordinate) -> None: """ Initializes a 2-dimensional point. :param x: X-coordinate. :param y: Y-coordinate. :raises TypeError: If x/y-coordinate is not of type integer or float. """ if not isinstance(x, (int, float)): ex = TypeError(f"Parameter 'x' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.") raise ex if not isinstance(y, (int, float)): ex = TypeError(f"Parameter 'y' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.") raise ex self.x = x self.y = y def Copy(self) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self: """ Create a new 2D-point as a copy of this 2D point. :returns: Copy of this 2D-point. .. seealso:: :meth:`+ operator <__add__>` Create a new 2D-point moved by a positive 2D-offset. :meth:`- operator <__sub__>` Create a new 2D-point moved by a negative 2D-offset. """ return self.__class__(self.x, self.y) def ToTuple(self) -> Tuple[Coordinate, Coordinate]: """ Convert this 2D-Point to a simple 2-element tuple. :returns: ``(x, y)`` tuple. """ return self.x, self.y def __add__(self, other: Any) -> "Point2D[Coordinate]": """ Adds a 2D-offset to this 2D-point and creates a new 2D-point. :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. :returns: A new 2D-point shifted by the 2D-offset. :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. """ if isinstance(other, Offset2D): return self.__class__( self.x + other.xOffset, self.y + other.yOffset ) elif isinstance(other, tuple): return self.__class__( self.x + other[0], self.y + other[1] ) else: ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __iadd__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self: """ Adds a 2D-offset to this 2D-point (inplace). :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. :returns: This 2D-point. :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. """ if isinstance(other, Offset2D): self.x += other.xOffset self.y += other.yOffset elif isinstance(other, tuple): self.x += other[0] self.y += other[1] else: ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self def __sub__(self, other: Any) -> Union["Offset2D[Coordinate]", "Point2D[Coordinate]"]: """ Subtract two 2D-Points from each other and create a new 2D-offset. :param other: A 2D-point as :class:`Point2D`. :returns: A new 2D-offset representing the distance between these two points. :raises TypeError: If parameter 'other' is not a :class:`Point2D`. """ if isinstance(other, Point2D): return Offset2D( self.x - other.x, self.y - other.y ) else: ex = TypeError(f"Parameter 'other' is not of type Point2D.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __isub__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self: """ Subtracts a 2D-offset to this 2D-point (inplace). :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. :returns: This 2D-point. :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. """ if isinstance(other, Offset2D): self.x -= other.xOffset self.y -= other.yOffset elif isinstance(other, tuple): self.x -= other[0] self.y -= other[1] else: ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self def __repr__(self) -> str: """ Returns the 2D point's string representation. :returns: The string representation of the 2D point. """ return f"Point2D({self.x}, {self.y})" def __str__(self) -> str: """ Returns the 2D point's string equivalent. :returns: The string equivalent of the 2D point. """ return f"({self.x}, {self.y})" @export class Origin2D(Point2D[Coordinate], Generic[Coordinate]): """An implementation of a 2D cartesian origin.""" def __init__(self) -> None: """ Initializes a 2-dimensional origin. """ super().__init__(0, 0) def Copy(self) -> "Origin2D[Coordinate]": # TODO: Python 3.11: -> Self: """ :raises RuntimeError: Because an origin can't be copied. """ raise RuntimeError(f"An origin can't be copied.") def __repr__(self) -> str: """ Returns the 2D origin's string representation. :returns: The string representation of the 2D origin. """ return f"Origin2D({self.x}, {self.y})" @export class Offset2D(Generic[Coordinate], metaclass=ExtendedType, slots=True): """An implementation of a 2D cartesian offset.""" xOffset: Coordinate #: The x-direction offset yOffset: Coordinate #: The y-direction offset def __init__(self, xOffset: Coordinate, yOffset: Coordinate) -> None: """ Initializes a 2-dimensional offset. :param xOffset: x-direction offset. :param yOffset: y-direction offset. :raises TypeError: If x/y-offset is not of type integer or float. """ if not isinstance(xOffset, (int, float)): ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.") raise ex if not isinstance(yOffset, (int, float)): ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.") raise ex self.xOffset = xOffset self.yOffset = yOffset def Copy(self) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self: """ Create a new 2D-offset as a copy of this 2D-offset. :returns: Copy of this 2D-offset. .. seealso:: :meth:`+ operator <__add__>` Create a new 2D-offset moved by a positive 2D-offset. :meth:`- operator <__sub__>` Create a new 2D-offset moved by a negative 2D-offset. """ return self.__class__(self.xOffset, self.yOffset) def ToTuple(self) -> Tuple[Coordinate, Coordinate]: """ Convert this 2D-offset to a simple 2-element tuple. :returns: ``(x, y)`` tuple. """ return self.xOffset, self.yOffset def __eq__(self, other) -> bool: """ Compare two 2D-offsets for equality. :param other: Parameter to compare against. :returns: ``True``, if both 2D-offsets are equal. :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`. """ if isinstance(other, Offset2D): return self.xOffset == other.xOffset and self.yOffset == other.yOffset elif isinstance(other, tuple): return self.xOffset == other[0] and self.yOffset == other[1] else: ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __ne__(self, other) -> bool: """ Compare two 2D-offsets for inequality. :param other: Parameter to compare against. :returns: ``True``, if both 2D-offsets are unequal. :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`. """ return not self.__eq__(other) def __neg__(self) -> "Offset2D[Coordinate]": """ Negate all components of this 2D-offset and create a new 2D-offset. :returns: 2D-offset with negated offset components. """ return self.__class__( -self.xOffset, -self.yOffset ) def __add__(self, other: Any) -> "Offset2D[Coordinate]": """ Adds a 2D-offset to this 2D-offset and creates a new 2D-offset. :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. :returns: A new 2D-offset extended by the 2D-offset. :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. """ if isinstance(other, Offset2D): return self.__class__( self.xOffset + other.xOffset, self.yOffset + other.yOffset ) elif isinstance(other, tuple): return self.__class__( self.xOffset + other[0], self.yOffset + other[1] ) else: ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __iadd__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self: """ Adds a 2D-offset to this 2D-offset (inplace). :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. :returns: This 2D-point. :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. """ if isinstance(other, Offset2D): self.xOffset += other.xOffset self.yOffset += other.yOffset elif isinstance(other, tuple): self.xOffset += other[0] self.yOffset += other[1] else: ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self def __sub__(self, other: Any) -> "Offset2D[Coordinate]": """ Subtracts a 2D-offset from this 2D-offset and creates a new 2D-offset. :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. :returns: A new 2D-offset reduced by the 2D-offset. :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. """ if isinstance(other, Offset2D): return self.__class__( self.xOffset - other.xOffset, self.yOffset - other.yOffset ) elif isinstance(other, tuple): return self.__class__( self.xOffset - other[0], self.yOffset - other[1] ) else: ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __isub__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self: """ Subtracts a 2D-offset from this 2D-offset (inplace). :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. :returns: This 2D-point. :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. """ if isinstance(other, Offset2D): self.xOffset -= other.xOffset self.yOffset -= other.yOffset elif isinstance(other, tuple): self.xOffset -= other[0] self.yOffset -= other[1] else: ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self def __repr__(self) -> str: """ Returns the 2D offset's string representation. :returns: The string representation of the 2D offset. """ return f"Offset2D({self.xOffset}, {self.yOffset})" def __str__(self) -> str: """ Returns the 2D offset's string equivalent. :returns: The string equivalent of the 2D offset. """ return f"({self.xOffset}, {self.yOffset})" @export class Size2D(Generic[Coordinate], metaclass=ExtendedType, slots=True): """An implementation of a 2D cartesian size.""" width: Coordinate #: width in x-direction. height: Coordinate #: height in y-direction. def __init__(self, width: Coordinate, height: Coordinate) -> None: """ Initializes a 2-dimensional size. :param width: width in x-direction. :param height: height in y-direction. :raises TypeError: If width/height is not of type integer or float. """ if not isinstance(width, (int, float)): ex = TypeError(f"Parameter 'width' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.") raise ex if not isinstance(height, (int, float)): ex = TypeError(f"Parameter 'height' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.") raise ex self.width = width self.height = height def Copy(self) -> "Size2D": # TODO: Python 3.11: -> Self: """ Create a new 2D-size as a copy of this 2D-size. :returns: Copy of this 2D-size. """ return self.__class__(self.width, self.height) def ToTuple(self) -> Tuple[Coordinate, Coordinate]: """ Convert this 2D-size to a simple 2-element tuple. :return: ``(width, height)`` tuple. """ return self.width, self.height def __repr__(self) -> str: """ Returns the 2D size's string representation. :returns: The string representation of the 2D size. """ return f"Size2D({self.width}, {self.height})" def __str__(self) -> str: """ Returns the 2D size's string equivalent. :returns: The string equivalent of the 2D size. """ return f"({self.width}, {self.height})" @export class Segment2D(Generic[Coordinate], metaclass=ExtendedType, slots=True): """An implementation of a 2D cartesian segment.""" start: Point2D[Coordinate] #: Start point of a segment. end: Point2D[Coordinate] #: End point of a segment. def __init__(self, start: Point2D[Coordinate], end: Point2D[Coordinate], copyPoints: bool = True) -> None: """ Initializes a 2-dimensional segment. :param start: Start point of the segment. :param end: End point of the segment. :raises TypeError: If start/end is not of type Point2D. """ if not isinstance(start, Point2D): ex = TypeError(f"Parameter 'start' is not of type Point2D.") ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.") raise ex if not isinstance(end, Point2D): ex = TypeError(f"Parameter 'end' is not of type Point2D.") ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.") raise ex self.start = start.Copy() if copyPoints else start self.end = end.Copy() if copyPoints else end @export class LineSegment2D(Segment2D[Coordinate], Generic[Coordinate]): """An implementation of a 2D cartesian line segment.""" @readonly def Length(self) -> float: """ Read-only property to return the Euclidean distance between start and end point. :return: Euclidean distance between start and end point """ return sqrt((self.end.x - self.start.x) ** 2 + (self.end.x - self.start.x) ** 2) def AngleTo(self, other: "LineSegment2D[Coordinate]") -> float: vectorA = self.ToOffset() vectorB = other.ToOffset() scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset return acos(scalarProductAB / (abs(self.Length) * abs(other.Length))) def ToOffset(self) -> Offset2D[Coordinate]: """ Convert this 2D line segment to a 2D-offset. :return: 2D-offset as :class:`Offset2D` """ return self.end - self.start def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate], Tuple[Coordinate, Coordinate]]: """ Convert this 2D line segment to a simple 2-element tuple of 2D-point tuples. :return: ``((x1, y1), (x2, y2))`` tuple. """ return self.start.ToTuple(), self.end.ToTuple() def __repr__(self) -> str: """ Returns the 2D line segment's string representation. :returns: The string representation of the 2D line segment. """ return f"LineSegment2D({self.start}, {self.end})" def __str__(self) -> str: """ Returns the 2D line segment's string equivalent. :returns: The string equivalent of the 2D line segment. """ return f"({self.start} → {self.end})" pyTooling-8.11.0/pyTooling/Cartesian3D/000077500000000000000000000000001513317154500176565ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Cartesian3D/Volumes.py000066400000000000000000000107701513317154500216670ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ _____ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ /| _ \ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ |_ \| | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |___) | |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|____/|____/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """An implementation of 3D cartesian volumes for Python.""" try: from pyTooling.Decorators import readonly, export from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType from pyTooling.Common import getFullyQualifiedName from pyTooling.Cartesian3D import Point3D, Offset3D except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Cartesian3D] Could not import from 'pyTooling.*'!") try: from Decorators import readonly, export from Exceptions import ToolingException from MetaClasses import ExtendedType from Common import getFullyQualifiedName from Cartesian3D import Point3D, Offset3D except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Cartesian3D] Could not import directly!") raise ex @export class Volume: """Base-class for all 3D cartesian volumes.""" @export class Cuboid(Volume): """ A cuboid is a volume made out of 6 rectangles. """ @export class Cube(Cuboid): """ A cube is a Cuboid made out of 6 equally sized squares. """ pyTooling-8.11.0/pyTooling/Cartesian3D/__init__.py000066400000000000000000000523471513317154500220020ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ _____ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ /| _ \ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ |_ \| | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |___) | |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|____/|____/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """An implementation of 3D cartesian data structures for Python.""" from math import sqrt, acos from typing import Union, Generic, Any, Tuple try: from pyTooling.Decorators import readonly, export from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType from pyTooling.Common import getFullyQualifiedName from pyTooling.Cartesian2D import Coordinate except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!") try: from Decorators import readonly, export from Exceptions import ToolingException from MetaClasses import ExtendedType from Common import getFullyQualifiedName from Cartesian2D import Coordinate except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Cartesian2D] Could not import directly!") raise ex @export class Point3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): """An implementation of a 3D cartesian point.""" x: Coordinate #: The x-direction coordinate. y: Coordinate #: The y-direction coordinate. z: Coordinate #: The z-direction coordinate. def __init__(self, x: Coordinate, y: Coordinate, z: Coordinate) -> None: """ Initializes a 3-dimensional point. :param x: X-coordinate. :param y: Y-coordinate. :param z: Z-coordinate. :raises TypeError: If x/y/z-coordinate is not of type integer or float. """ if not isinstance(x, (int, float)): ex = TypeError(f"Parameter 'x' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.") raise ex if not isinstance(y, (int, float)): ex = TypeError(f"Parameter 'y' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.") raise ex if not isinstance(z, (int, float)): ex = TypeError(f"Parameter 'z' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(z)}'.") raise ex self.x = x self.y = y self.z = z def Copy(self) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self: """ Create a new 3D-point as a copy of this 3D point. :return: Copy of this 3D-point. .. seealso:: :meth:`+ operator <__add__>` Create a new 3D-point moved by a positive 3D-offset. :meth:`- operator <__sub__>` Create a new 3D-point moved by a negative 3D-offset. """ return self.__class__(self.x, self.y, self.z) def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]: """ Convert this 3D-Point to a simple 3-element tuple. :return: ``(x, y, z)`` tuple. """ return self.x, self.y, self.z def __add__(self, other: Any) -> "Point3D[Coordinate]": """ Adds a 3D-offset to this 3D-point and creates a new 3D-point. :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. :return: A new 3D-point shifted by the 3D-offset. :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. """ if isinstance(other, Offset3D): return self.__class__( self.x + other.xOffset, self.y + other.yOffset, self.z + other.zOffset ) elif isinstance(other, tuple): return self.__class__( self.x + other[0], self.y + other[1], self.z + other[2] ) else: ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __iadd__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self: """ Adds a 3D-offset to this 3D-point (inplace). :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. :return: This 3D-point. :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. """ if isinstance(other, Offset3D): self.x += other.xOffset self.y += other.yOffset self.z += other.zOffset elif isinstance(other, tuple): self.x += other[0] self.y += other[1] self.z += other[2] else: ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self def __sub__(self, other: Any) -> Union["Offset3D[Coordinate]", "Point3D[Coordinate]"]: """ Subtract two 3D-Points from each other and create a new 3D-offset. :param other: A 3D-point as :class:`Point3D`. :return: A new 3D-offset representing the distance between these two points. :raises TypeError: If parameter 'other' is not a :class:`Point3D`. """ if isinstance(other, Point3D): return Offset3D( self.x - other.x, self.y - other.y, self.z - other.z ) else: ex = TypeError(f"Parameter 'other' is not of type Point3D.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __isub__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self: """ Subtracts a 3D-offset to this 3D-point (inplace). :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. :return: This 3D-point. :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. """ if isinstance(other, Offset3D): self.x -= other.xOffset self.y -= other.yOffset self.z -= other.zOffset elif isinstance(other, tuple): self.x -= other[0] self.y -= other[1] self.z -= other[2] else: ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self def __repr__(self) -> str: """ Returns the 3D point's string representation. :returns: The string representation of the 3D point. """ return f"Point3D({self.x}, {self.y}, {self.z})" def __str__(self) -> str: """ Returns the 3D point's string equivalent. :returns: The string equivalent of the 3D point. """ return f"({self.x}, {self.y}, {self.z})" @export class Origin3D(Point3D[Coordinate], Generic[Coordinate]): """An implementation of a 3D cartesian origin.""" def __init__(self) -> None: """ Initializes a 3-dimensional origin. """ super().__init__(0, 0, 0) def Copy(self) -> "Origin3D[Coordinate]": # TODO: Python 3.11: -> Self: """ :raises RuntimeError: Because an origin can't be copied. """ raise RuntimeError(f"An origin can't be copied.") def __repr__(self) -> str: """ Returns the 3D origin's string representation. :returns: The string representation of the 3D origin. """ return f"Origin3D({self.x}, {self.y}, {self.z})" @export class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): """An implementation of a 3D cartesian offset.""" xOffset: Coordinate #: The x-direction offset yOffset: Coordinate #: The y-direction offset zOffset: Coordinate #: The z-direction offset def __init__(self, xOffset: Coordinate, yOffset: Coordinate, zOffset: Coordinate) -> None: """ Initializes a 3-dimensional offset. :param xOffset: x-direction offset. :param yOffset: y-direction offset. :param zOffset: z-direction offset. :raises TypeError: If x/y/z-offset is not of type integer or float. """ if not isinstance(xOffset, (int, float)): ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.") raise ex if not isinstance(yOffset, (int, float)): ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.") raise ex if not isinstance(zOffset, (int, float)): ex = TypeError(f"Parameter 'zOffset' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(zOffset)}'.") raise ex self.xOffset = xOffset self.yOffset = yOffset self.zOffset = zOffset def Copy(self) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self: """ Create a new 3D-offset as a copy of this 3D-offset. :returns: Copy of this 3D-offset. .. seealso:: :meth:`+ operator <__add__>` Create a new 3D-offset moved by a positive 3D-offset. :meth:`- operator <__sub__>` Create a new 3D-offset moved by a negative 3D-offset. """ return self.__class__(self.xOffset, self.yOffset, self.zOffset) def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]: """ Convert this 3D-offset to a simple 3-element tuple. :returns: ``(x, y, z)`` tuple. """ return self.xOffset, self.yOffset, self.zOffset def __eq__(self, other) -> bool: """ Compare two 3D-offsets for equality. :param other: Parameter to compare against. :returns: ``True``, if both 3D-offsets are equal. :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`. """ if isinstance(other, Offset3D): return self.xOffset == other.xOffset and self.yOffset == other.yOffset and self.zOffset == other.zOffset elif isinstance(other, tuple): return self.xOffset == other[0] and self.yOffset == other[1] and self.zOffset == other[2] else: ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __ne__(self, other) -> bool: """ Compare two 3D-offsets for inequality. :param other: Parameter to compare against. :returns: ``True``, if both 3D-offsets are unequal. :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`. """ return not self.__eq__(other) def __neg__(self) -> "Offset3D[Coordinate]": """ Negate all components of this 3D-offset and create a new 3D-offset. :returns: 3D-offset with negated offset components. """ return self.__class__( -self.xOffset, -self.yOffset, -self.zOffset ) def __add__(self, other: Any) -> "Offset3D[Coordinate]": """ Adds a 3D-offset to this 3D-offset and creates a new 3D-offset. :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. :returns: A new 3D-offset extended by the 3D-offset. :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. """ if isinstance(other, Offset3D): return self.__class__( self.xOffset + other.xOffset, self.yOffset + other.yOffset, self.zOffset + other.zOffset ) elif isinstance(other, tuple): return self.__class__( self.xOffset + other[0], self.yOffset + other[1], self.zOffset + other[2] ) else: ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __iadd__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self: """ Adds a 3D-offset to this 3D-offset (inplace). :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. :returns: This 3D-point. :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. """ if isinstance(other, Offset3D): self.xOffset += other.xOffset self.yOffset += other.yOffset self.zOffset += other.zOffset elif isinstance(other, tuple): self.xOffset += other[0] self.yOffset += other[1] self.zOffset += other[2] else: ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self def __sub__(self, other: Any) -> "Offset3D[Coordinate]": """ Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset. :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. :returns: A new 3D-offset reduced by the 3D-offset. :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. """ if isinstance(other, Offset3D): return self.__class__( self.xOffset - other.xOffset, self.yOffset - other.yOffset, self.zOffset - other.zOffset ) elif isinstance(other, tuple): return self.__class__( self.xOffset - other[0], self.yOffset - other[1], self.zOffset - other[2] ) else: ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex def __isub__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self: """ Subtracts a 3D-offset from this 3D-offset (inplace). :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. :returns: This 3D-point. :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. """ if isinstance(other, Offset3D): self.xOffset -= other.xOffset self.yOffset -= other.yOffset self.zOffset -= other.zOffset elif isinstance(other, tuple): self.xOffset -= other[0] self.yOffset -= other[1] self.zOffset -= other[2] else: ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self def __repr__(self) -> str: """ Returns the 3D offset's string representation. :returns: The string representation of the 3D offset. """ return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})" def __str__(self) -> str: """ Returns the 3D offset's string equivalent. :returns: The string equivalent of the 3D offset. """ return f"({self.xOffset}, {self.yOffset}, {self.zOffset})" @export class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): """An implementation of a 3D cartesian size.""" width: Coordinate #: width in x-direction. height: Coordinate #: height in y-direction. depth: Coordinate #: depth in z-direction. def __init__(self, width: Coordinate, height: Coordinate, depth: Coordinate) -> None: """ Initializes a 2-dimensional size. :param width: width in x-direction. :param height: height in y-direction. :param depth: depth in z-direction. :raises TypeError: If width/height/depth is not of type integer or float. """ if not isinstance(width, (int, float)): ex = TypeError(f"Parameter 'width' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.") raise ex if not isinstance(height, (int, float)): ex = TypeError(f"Parameter 'height' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.") raise ex if not isinstance(depth, (int, float)): ex = TypeError(f"Parameter 'depth' is not of type integer or float.") ex.add_note(f"Got type '{getFullyQualifiedName(depth)}'.") raise ex self.width = width self.height = height self.depth = depth def Copy(self) -> "Size3D[Coordinate]": # TODO: Python 3.11: -> Self: """ Create a new 3D-size as a copy of this 3D-size. :returns: Copy of this 3D-size. """ return self.__class__(self.width, self.height, self.depth) def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]: """ Convert this 3D-size to a simple 3-element tuple. :return: ``(width, height, depth)`` tuple. """ return self.width, self.height, self.depth def __repr__(self) -> str: """ Returns the 3D size's string representation. :returns: The string representation of the 3D size. """ return f"Size3D({self.width}, {self.height}, {self.depth})" def __str__(self) -> str: """ Returns the 3D size's string equivalent. :returns: The string equivalent of the 3D size. """ return f"({self.width}, {self.height}, {self.depth})" @export class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): """An implementation of a 3D cartesian segment.""" start: Point3D[Coordinate] #: Start point of a segment. end: Point3D[Coordinate] #: End point of a segment. def __init__(self, start: Point3D[Coordinate], end: Point3D[Coordinate], copyPoints: bool = True) -> None: """ Initializes a 3-dimensional segment. :param start: Start point of the segment. :param end: End point of the segment. :raises TypeError: If start/end is not of type Point3D. """ if not isinstance(start, Point3D): ex = TypeError(f"Parameter 'start' is not of type Point3D.") ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.") raise ex if not isinstance(end, Point3D): ex = TypeError(f"Parameter 'end' is not of type Point3D.") ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.") raise ex self.start = start.Copy() if copyPoints else start self.end = end.Copy() if copyPoints else end @export class LineSegment3D(Segment3D[Coordinate], Generic[Coordinate]): """An implementation of a 3D cartesian line segment.""" @readonly def Length(self) -> float: """ Read-only property to return the Euclidean distance between start and end point. :return: Euclidean distance between start and end point """ return sqrt((self.end.x - self.start.x) ** 2 + (self.end.y - self.start.y) ** 2 + (self.end.z - self.start.z) ** 2) def AngleTo(self, other: "LineSegment3D[Coordinate]") -> float: vectorA = self.ToOffset() vectorB = other.ToOffset() scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset + vectorA.zOffset * vectorB.zOffset return acos(scalarProductAB / (abs(self.Length) * abs(other.Length))) def ToOffset(self) -> Offset3D[Coordinate]: """ Convert this 3D line segment to a 3D-offset. :return: 3D-offset as :class:`Offset3D` """ return self.end - self.start def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate, Coordinate], Tuple[Coordinate, Coordinate, Coordinate]]: """ Convert this 3D line segment to a simple 2-element tuple of 3D-point tuples. :return: ``((x1, y1, z1), (x2, y2, z2))`` tuple. """ return self.start.ToTuple(), self.end.ToTuple() def __repr__(self) -> str: """ Returns the 3D line segment's string representation. :returns: The string representation of the 3D line segment. """ return f"LineSegment3D({self.start}, {self.end})" def __str__(self) -> str: """ Returns the 3D line segment's string equivalent. :returns: The string equivalent of the 3D line segment. """ return f"({self.start} → {self.end})" pyTooling-8.11.0/pyTooling/Common/000077500000000000000000000000001513317154500170065ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Common/__init__.py000066400000000000000000000416621513317154500211300ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ ___ _ __ ___ ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | | | | | | | | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_| |_|_| |_| |_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Common types, helper functions and classes. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ __author__ = "Patrick Lehmann" __email__ = "Paebbels@gmail.com" __copyright__ = "2017-2026, Patrick Lehmann" __license__ = "Apache License, Version 2.0" __version__ = "8.11.0" __keywords__ = [ "abstract", "argparse", "attributes", "bfs", "cli", "console", "data structure", "decorators", "dfs", "double linked list", "exceptions", "file system statistics", "generators", "generic library", "generic path", "geometry", "graph", "installation", "iterators", "licensing", "linked list", "message logging", "meta-classes", "overloading", "override", "packaging", "path", "platform", "setuptools", "shapes", "shell", "singleton", "slots", "terminal", "text user interface", "stopwatch", "tree", "TUI", "url", "versioning", "volumes", "warning", "wheel" ] __issue_tracker__ = "https://GitHub.com/pyTooling/pyTooling/issues" from collections import deque from importlib.resources import files from numbers import Number from os import chdir from pathlib import Path from types import ModuleType, TracebackType from typing import Type, TypeVar, Callable, Generator, Hashable, List from typing import Any, Dict, Tuple, Union, Mapping, Set, Iterable, Optional as Nullable try: from pyTooling.Decorators import export except ModuleNotFoundError: # pragma: no cover print("[pyTooling.Common] Could not import from 'pyTooling.*'!") try: from Decorators import export except ModuleNotFoundError as ex: # pragma: no cover print("[pyTooling.Common] Could not import directly!") raise ex @export def getFullyQualifiedName(obj: Any) -> str: """ Assemble the fully qualified name of a type. :param obj: The object for with the fully qualified type is to be assembled. :returns: The fully qualified name of obj's type. """ try: module = obj.__module__ # for class or function except AttributeError: module = obj.__class__.__module__ try: name = obj.__qualname__ # for class or function except AttributeError: name = obj.__class__.__qualname__ # If obj is a method of builtin class, then module will be None if module == "builtins" or module is None: return name return f"{module}.{name}" @export def getResourceFile(module: Union[str, ModuleType], filename: str) -> Path: """ Compute the path to a file within a resource package. :param module: The resource package. :param filename: The filename. :returns: Path to the resource's file. :raises ToolingException: If resource file doesn't exist. """ # TODO: files() has wrong TypeHint Traversible vs. Path resourcePath: Path = files(module) / filename if not resourcePath.exists(): from pyTooling.Exceptions import ToolingException raise ToolingException(f"Resource file '{filename}' not found in resource '{module}'.") \ from FileNotFoundError(str(resourcePath)) return resourcePath @export def readResourceFile(module: Union[str, ModuleType], filename: str) -> str: """ Read a text file resource from resource package. :param module: The resource package. :param filename: The filename. :returns: File content. """ # TODO: check if resource exists. return files(module).joinpath(filename).read_text() @export def isnestedclass(cls: Type, scope: Type) -> bool: """ Returns true, if the given class ``cls`` is a member on an outer class ``scope``. :param cls: Class to check, if it's a nested class. :param scope: Outer class which is the outer scope of ``cls``. :returns: ``True``, if ``cls`` is a nested class within ``scope``. """ for mroClass in scope.mro(): for memberName in mroClass.__dict__: member = getattr(mroClass, memberName) if isinstance(member, Type): if cls is member: return True return False @export def getsizeof(obj: Any) -> int: """ Recursively calculate the "true" size of an object including complex members like ``__dict__``. :param obj: Object to calculate the size of. :returns: True size of an object in bytes. .. admonition:: Background Information The function :func:`sys.getsizeof` only returns the raw size of a Python object and doesn't account for the overhead of e.g. ``_dict__`` to store dynamically allocated object members. .. seealso:: The code is based on code snippets and ideas from: * `Compute Memory Footprint of an Object and its Contents `__ (MIT Lizense) * `How do I determine the size of an object in Python? `__ (CC BY-SA 4.0) * `Python __slots__, slots, and object layout `__ (MIT Lizense) """ from sys import getsizeof as sys_getsizeof visitedIDs = set() #: A set to track visited objects, so memory consumption isn't counted multiple times. def recurse(obj: Any) -> int: """ Nested function for recursion. :param obj: Subobject to calculate the size of. :returns: Size of a subobject in bytes. """ # If already visited, return 0 bytes, so no additional bytes are accumulated objectID = id(obj) if objectID in visitedIDs: return 0 else: visitedIDs.add(objectID) # Get objects raw size size: int = sys_getsizeof(obj) # Skip elementary types if isinstance(obj, (str, bytes, bytearray, range, Number)): pass # Handle iterables elif isinstance(obj, (tuple, list, Set, deque)): # TODO: What about builtin "set", "frozenset" and "dict"? for item in obj: size += recurse(item) # Handle mappings elif isinstance(obj, Mapping) or hasattr(obj, 'items'): items = getattr(obj, 'items') # Check if obj.items is a bound method. if hasattr(items, "__self__"): itemView = items() else: itemView = {} # bind(obj, items) for key, value in itemView: size += recurse(key) + recurse(value) # Accumulate members from __dict__ if hasattr(obj, '__dict__'): v = vars(obj) size += recurse(v) # Accumulate members from __slots__ if hasattr(obj, '__slots__') and obj.__slots__ is not None: for slot in obj.__slots__: if hasattr(obj, slot): size += recurse(getattr(obj, slot)) return size return recurse(obj) def bind(instance, func, methodName: Nullable[str] = None): """ Bind the function *func* to *instance*, with either provided name *as_name* or the existing name of *func*. The provided *func* should accept the instance as the first argument, i.e. "self". :param instance: :param func: :param methodName: :return: """ if methodName is None: methodName = func.__name__ boundMethod = func.__get__(instance, instance.__class__) setattr(instance, methodName, boundMethod) return boundMethod @export def count(iterator: Iterable) -> int: """ Returns the number of elements in an iterable. .. attention:: After counting the iterable's elements, the iterable is consumed. :param iterator: Iterable to consume and count. :return: Number of elements in the iterable. """ return len(list(iterator)) _Element = TypeVar("Element") @export def firstElement(indexable: Union[List[_Element], Tuple[_Element, ...]]) -> _Element: """ Returns the first element from an indexable. :param indexable: Indexable to get the first element from. :return: First element. """ return indexable[0] @export def lastElement(indexable: Union[List[_Element], Tuple[_Element, ...]]) -> _Element: """ Returns the last element from an indexable. :param indexable: Indexable to get the last element from. :return: Last element. """ return indexable[-1] @export def firstItem(iterable: Iterable[_Element]) -> _Element: """ Returns the first item from an iterable. :param iterable: Iterable to get the first item from. :return: First item. :raises ValueError: If parameter 'iterable' contains no items. """ i = iter(iterable) try: return next(i) except StopIteration: raise ValueError(f"Iterable contains no items.") @export def lastItem(iterable: Iterable[_Element]) -> _Element: """ Returns the last item from an iterable. :param iterable: Iterable to get the last item from. :return: Last item. :raises ValueError: If parameter 'iterable' contains no items. """ i = iter(iterable) try: element = next(i) except StopIteration: raise ValueError(f"Iterable contains no items.") for element in i: pass return element _DictKey = TypeVar("_DictKey") _DictKey1 = TypeVar("_DictKey1") _DictKey2 = TypeVar("_DictKey2") _DictKey3 = TypeVar("_DictKey3") _DictValue1 = TypeVar("_DictValue1") _DictValue2 = TypeVar("_DictValue2") _DictValue3 = TypeVar("_DictValue3") @export def firstKey(d: Dict[_DictKey1, _DictValue1]) -> _DictKey1: """ Retrieves the first key from a dictionary's keys. :param d: Dictionary to get the first key from. :returns: The first key. :raises ValueError: If parameter 'd' is an empty dictionary. """ if len(d) == 0: raise ValueError(f"Dictionary is empty.") return next(iter(d.keys())) @export def firstValue(d: Dict[_DictKey1, _DictValue1]) -> _DictValue1: """ Retrieves the first value from a dictionary's values. :param d: Dictionary to get the first value from. :returns: The first value. :raises ValueError: If parameter 'd' is an empty dictionary. """ if len(d) == 0: raise ValueError(f"Dictionary is empty.") return next(iter(d.values())) @export def firstPair(d: Dict[_DictKey1, _DictValue1]) -> Tuple[_DictKey1, _DictValue1]: """ Retrieves the first key-value-pair from a dictionary. :param d: Dictionary to get the first key-value-pair from. :returns: The first key-value-pair as tuple. :raises ValueError: If parameter 'd' is an empty dictionary. """ if len(d) == 0: raise ValueError(f"Dictionary is empty.") return next(iter(d.items())) @export def mergedicts(*dicts: Dict, filter: Nullable[Callable[[Hashable, Any], bool]] = None) -> Dict: """ Merge multiple dictionaries into a single new dictionary. If parameter ``filter`` isn't ``None``, then this function is applied to every element during the merge operation. If it returns true, the dictionary element will be present in the resulting dictionary. :param dicts: Tuple of dictionaries to merge as positional parameters. :param filter: Optional filter function to apply to each dictionary element when merging. :returns: A new dictionary containing the merge result. :raises ValueError: If 'mergedicts' got called without any dictionaries parameters. .. seealso:: `How do I merge two dictionaries in a single expression in Python? `__ """ if len(dicts) == 0: raise ValueError(f"Called 'mergedicts' without any dictionary parameter.") if filter is None: return {k: v for d in dicts for k, v in d.items()} else: return {k: v for d in dicts for k, v in d.items() if filter(k, v)} @export def zipdicts(*dicts: Dict) -> Generator[Tuple, None, None]: """ Iterate multiple dictionaries simultaneously. :param dicts: Tuple of dictionaries to iterate as positional parameters. :returns: A generator returning a tuple containing the key and values of each dictionary in the order of given dictionaries. :raises ValueError: If 'zipdicts' got called without any dictionary parameters. :raises ValueError: If not all dictionaries have the same length. .. seealso:: The code is based on code snippets and ideas from: * `zipping together Python dicts `__ (MIT Lizense) """ if len(dicts) == 0: raise ValueError(f"Called 'zipdicts' without any dictionary parameter.") if any(len(d) != len(dicts[0]) for d in dicts): raise ValueError(f"All given dictionaries must have the same length.") def gen(ds: Tuple[Dict, ...]) -> Generator[Tuple, None, None]: for key, item0 in ds[0].items(): # WORKAROUND: using redundant parenthesis for Python 3.7 and pypy-3.10 yield key, item0, *(d[key] for d in ds[1:]) return gen(dicts) @export class ChangeDirectory: """ A context manager for changing a directory. """ _oldWorkingDirectory: Path #: Working directory before directory change. _newWorkingDirectory: Path #: New working directory. def __init__(self, directory: Path) -> None: """ Initializes the context manager for changing directories. :param directory: The new working directory to change into. """ self._newWorkingDirectory = directory def __enter__(self) -> Path: """ Enter the context and change the working directory to the parameter given in the class initializer. :returns: The relative path between old and new working directories. """ self._oldWorkingDirectory = Path.cwd() chdir(self._newWorkingDirectory) if self._newWorkingDirectory.is_absolute(): return self._newWorkingDirectory.resolve() else: return (self._oldWorkingDirectory / self._newWorkingDirectory).resolve() def __exit__( self, exc_type: Nullable[Type[BaseException]] = None, exc_val: Nullable[BaseException] = None, exc_tb: Nullable[TracebackType] = None ) -> Nullable[bool]: """ Exit the context and revert any working directory changes. :param exc_type: Exception type :param exc_val: Exception instance :param exc_tb: Exception's traceback. :returns: ``None`` """ chdir(self._oldWorkingDirectory) pyTooling-8.11.0/pyTooling/Configuration/000077500000000000000000000000001513317154500203655ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Configuration/JSON.py000066400000000000000000000335441513317154500215210ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ __ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Configuration reader for JSON files. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from json import load from pathlib import Path from typing import Dict, List, Union, Iterator as typing_Iterator, Self try: from pyTooling.Decorators import export from pyTooling.MetaClasses import ExtendedType from pyTooling.Configuration import ConfigurationException, KeyT, NodeT, ValueT from pyTooling.Configuration import Node as Abstract_Node from pyTooling.Configuration import Dictionary as Abstract_Dict from pyTooling.Configuration import Sequence as Abstract_Seq from pyTooling.Configuration import Configuration as Abstract_Configuration except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Configuration.JSON] Could not import from 'pyTooling.*'!") try: from Decorators import export from MetaClasses import ExtendedType from pyTooling.Configuration import ConfigurationException, KeyT, NodeT, ValueT from pyTooling.Configuration import Node as Abstract_Node from pyTooling.Configuration import Dictionary as Abstract_Dict from pyTooling.Configuration import Sequence as Abstract_Seq from pyTooling.Configuration import Configuration as Abstract_Configuration except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Configuration.JSON] Could not import directly!") raise ex @export class Node(Abstract_Node): """ Node in a JSON configuration data structure. """ _jsonNode: Union[Dict, List] #: Reference to the associated JSON node. _cache: Dict[str, ValueT] _key: KeyT #: Key of this node. _length: int #: Number of sub-elements. def __init__( self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: Union[Dict, List] ) -> None: """ Initializes a JSON node. :param root: Reference to the root node. :param parent: Reference to the parent node. :param key: :param jsonNode: Reference to the JSON node. """ Abstract_Node.__init__(self, root, parent) self._jsonNode = jsonNode self._cache = {} self._key = key self._length = len(jsonNode) def __len__(self) -> int: """ Returns the number of sub-elements. :returns: Number of sub-elements. """ return self._length def __getitem__(self, key: KeyT) -> ValueT: """ Access an element in the node by index or key. :param key: Index or key of the element. :returns: A node (sequence or dictionary) or scalar value (int, float, str). """ return self._GetNodeOrValue(str(key)) @property def Key(self) -> KeyT: """ Property to access the node's key. :returns: Key of the node. """ return self._key @Key.setter def Key(self, value: KeyT) -> None: raise NotImplementedError() def QueryPath(self, query: str) -> ValueT: """ Return a node or value based on a path description to that node or value. :param query: String describing the path to the node or value. :returns: A node (sequence or dictionary) or scalar value (int, float, str). """ path = self._ToPath(query) return self._GetNodeOrValueByPathExpression(path) @staticmethod def _ToPath(query: str) -> List[Union[str, int]]: return query.split(":") def _GetNodeOrValue(self, key: str) -> ValueT: try: value = self._cache[key] except KeyError: try: value = self._jsonNode[key] except (KeyError, TypeError): try: value = self._jsonNode[int(key)] except KeyError: try: value = self._jsonNode[float(key)] except KeyError as ex: raise Exception(f"") from ex # XXX: needs error message if isinstance(value, str): value = self._ResolveVariables(value) elif isinstance(value, (int, float)): value = str(value) elif isinstance(value, dict): value = self.DICT_TYPE(self, self, key, value) elif isinstance(value, list): value = self.SEQ_TYPE(self, self, key, value) else: raise Exception(f"") from TypeError(f"Unknown type '{value.__class__.__name__}' returned from json.") # XXX: error message self._cache[key] = value return value def _ResolveVariables(self, value: str) -> str: if value == "": return "" elif "$" not in value: return value rawValue = value result = "" while (len(rawValue) > 0): # print(f"_ResolveVariables: LOOP rawValue='{rawValue}'") beginPos = rawValue.find("$") if beginPos < 0: result += rawValue rawValue = "" else: result += rawValue[:beginPos] if rawValue[beginPos + 1] == "$": result += "$" rawValue = rawValue[1:] elif rawValue[beginPos + 1] == "{": endPos = rawValue.find("}", beginPos) nextPos = rawValue.rfind("$", beginPos, endPos) if endPos < 0: raise Exception(f"") # XXX: InterpolationSyntaxError(option, section, f"Bad interpolation variable reference {rest!r}") if (nextPos > 0) and (nextPos < endPos): # an embedded $-sign path = rawValue[nextPos+2:endPos] # print(f"_ResolveVariables: path='{path}'") innervalue = self._GetValueByPathExpression(self._ToPath(path)) # print(f"_ResolveVariables: innervalue='{innervalue}'") rawValue = rawValue[beginPos:nextPos] + str(innervalue) + rawValue[endPos + 1:] # print(f"_ResolveVariables: new rawValue='{rawValue}'") else: path = rawValue[beginPos+2:endPos] rawValue = rawValue[endPos+1:] result += str(self._GetValueByPathExpression(self._ToPath(path))) return result def _GetValueByPathExpression(self, path: List[KeyT]) -> ValueT: node = self for p in path: if p == "..": node = node._parent else: node = node._GetNodeOrValue(p) if isinstance(node, Dictionary): raise Exception(f"Error when resolving path expression '{':'.join(path)}' at '{p}'.") from TypeError(f"") # XXX: needs error messages return node def _GetNodeOrValueByPathExpression(self, path: List[KeyT]) -> ValueT: node = self for p in path: if p == "..": node = node._parent else: node = node._GetNodeOrValue(p) return node @export class Dictionary(Node, Abstract_Dict): """A dictionary node in a JSON data file.""" _keys: List[KeyT] #: List of keys in this dictionary. def __init__( self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: Dict ) -> None: """ Initializes a JSON dictionary. :param root: Reference to the root node. :param parent: Reference to the parent node. :param key: :param jsonNode: Reference to the JSON node. """ Node.__init__(self, root, parent, key, jsonNode) self._keys = [str(k) for k in jsonNode.keys()] def __contains__(self, key: KeyT) -> bool: """ Checks if the key is in this dictionary. :param key: The key to check. :returns: ``True``, if the key is in the dictionary. """ return key in self._keys def __iter__(self) -> typing_Iterator[ValueT]: """ Returns an iterator to iterate dictionary keys. :returns: Dictionary key iterator. """ class Iterator(metaclass=ExtendedType, slots=True): """Iterator to iterate dictionary items.""" _iter: typing_Iterator _obj: Dictionary def __init__(self, obj: Dictionary) -> None: """ Initializes an iterator for a JSON dictionary node. :param obj: JSON dictionary to iterate. """ self._iter = iter(obj._keys) self._obj = obj def __iter__(self) -> Self: """ Return itself to fulfil the iterator protocol. :returns: Itself. """ return self # pragma: no cover def __next__(self) -> ValueT: """ Returns the next item in the dictionary. :returns: Next item. """ key = next(self._iter) return self._obj[key] return Iterator(self) @export class Sequence(Node, Abstract_Seq): """A sequence node (ordered list) in a JSON data file.""" def __init__( self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: List ) -> None: """ Initializes a JSON sequence (list). :param root: Reference to the root node. :param parent: Reference to the parent node. :param key: :param jsonNode: Reference to the JSON node. """ Node.__init__(self, root, parent, key, jsonNode) self._length = len(jsonNode) def __iter__(self) -> typing_Iterator[ValueT]: """ Returns an iterator to iterate items in the sequence of sub-nodes. :returns: Iterator to iterate items in a sequence. """ class Iterator(metaclass=ExtendedType, slots=True): """Iterator to iterate sequence items.""" _i: int #: internal iterator position _obj: Sequence #: Sequence object to iterate def __init__(self, obj: Sequence) -> None: """ Initializes an iterator for a JSON sequence node. :param obj: YAML sequence to iterate. """ self._i = 0 self._obj = obj def __iter__(self) -> Self: """ Return itself to fulfil the iterator protocol. :returns: Itself. """ return self # pragma: no cover def __next__(self) -> ValueT: """ Returns the next item in the sequence. :returns: Next item. :raises StopIteration: If end of sequence is reached. """ try: result = self._obj[str(self._i)] self._i += 1 return result except IndexError: raise StopIteration return Iterator(self) setattr(Node, "DICT_TYPE", Dictionary) setattr(Node, "SEQ_TYPE", Sequence) @export class Configuration(Dictionary, Abstract_Configuration): """A configuration read from a JSON file.""" _jsonConfig: Dict def __init__(self, configFile: Path) -> None: """ Initializes a configuration instance that reads a JSON file as input. All sequence items or dictionaries key-value-pairs in the JSON file are accessible via Python's dictionary syntax. :param configFile: Configuration file to read and parse. """ if not configFile.exists(): raise ConfigurationException(f"JSON configuration file '{configFile}' not found.") from FileNotFoundError(configFile) with configFile.open("r", encoding="utf-8") as file: self._jsonConfig = load(file) Dictionary.__init__(self, self, self, None, self._jsonConfig) Abstract_Configuration.__init__(self, configFile) def __getitem__(self, key: str) -> ValueT: """ Access a configuration node by key. :param key: The key to look for. :returns: A node (sequence or dictionary) or scalar value (int, float, str). """ return self._GetNodeOrValue(str(key)) def __setitem__(self, key: str, value: ValueT) -> None: raise NotImplementedError() pyTooling-8.11.0/pyTooling/Configuration/YAML.py000066400000000000000000000343301513317154500215040ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ __ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Configuration reader for YAML files. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from pathlib import Path from typing import Dict, List, Union, Iterator as typing_Iterator, Self try: from ruamel.yaml import YAML, CommentedMap, CommentedSeq except ImportError as ex: # pragma: no cover raise Exception("Optional dependency 'ruamel.yaml' not installed. Either install pyTooling with extra dependencies 'pyTooling[yaml]' or install 'ruamel.yaml' directly.") from ex try: from pyTooling.Decorators import export from pyTooling.MetaClasses import ExtendedType from pyTooling.Configuration import ConfigurationException, KeyT, NodeT, ValueT from pyTooling.Configuration import Node as Abstract_Node from pyTooling.Configuration import Dictionary as Abstract_Dict from pyTooling.Configuration import Sequence as Abstract_Seq from pyTooling.Configuration import Configuration as Abstract_Configuration except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Configuration.YAML] Could not import from 'pyTooling.*'!") try: from Decorators import export from MetaClasses import ExtendedType from pyTooling.Configuration import ConfigurationException, KeyT, NodeT, ValueT from pyTooling.Configuration import Node as Abstract_Node from pyTooling.Configuration import Dictionary as Abstract_Dict from pyTooling.Configuration import Sequence as Abstract_Seq from pyTooling.Configuration import Configuration as Abstract_Configuration except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Configuration.YAML] Could not import directly!") raise ex @export class Node(Abstract_Node): """ Node in a YAML configuration data structure. """ _yamlNode: Union[CommentedMap, CommentedSeq] #: Reference to the associated YAML node. _cache: Dict[str, ValueT] _key: KeyT #: Key of this node. _length: int #: Number of sub-elements. def __init__( self, root: "Configuration", parent: NodeT, key: KeyT, yamlNode: Union[CommentedMap, CommentedSeq] ) -> None: """ Initializes a YAML node. :param root: Reference to the root node. :param parent: Reference to the parent node. :param key: :param yamlNode: Reference to the YAML node. """ Abstract_Node.__init__(self, root, parent) self._yamlNode = yamlNode self._cache = {} self._key = key self._length = len(yamlNode) def __len__(self) -> int: """ Returns the number of sub-elements. :returns: Number of sub-elements. """ return self._length def __getitem__(self, key: KeyT) -> ValueT: """ Access an element in the node by index or key. :param key: Index or key of the element. :returns: A node (sequence or dictionary) or scalar value (int, float, str). """ return self._GetNodeOrValue(str(key)) @property def Key(self) -> KeyT: """ Property to access the node's key. :returns: Key of the node. """ return self._key @Key.setter def Key(self, value: KeyT) -> None: raise NotImplementedError() def QueryPath(self, query: str) -> ValueT: """ Return a node or value based on a path description to that node or value. :param query: String describing the path to the node or value. :returns: A node (sequence or dictionary) or scalar value (int, float, str). """ path = self._ToPath(query) return self._GetNodeOrValueByPathExpression(path) @staticmethod def _ToPath(query: str) -> List[Union[str, int]]: return query.split(":") def _GetNodeOrValue(self, key: str) -> ValueT: try: value = self._cache[key] except KeyError: try: value = self._yamlNode[key] except (KeyError, TypeError): try: value = self._yamlNode[int(key)] except KeyError: try: value = self._yamlNode[float(key)] except KeyError as ex: raise Exception(f"") from ex # XXX: needs error message if isinstance(value, str): value = self._ResolveVariables(value) elif isinstance(value, (int, float)): value = str(value) elif isinstance(value, CommentedMap): value = self.DICT_TYPE(self, self, key, value) elif isinstance(value, CommentedSeq): value = self.SEQ_TYPE(self, self, key, value) else: raise Exception(f"") from TypeError(f"Unknown type '{value.__class__.__name__}' returned from ruamel.yaml.") # XXX: error message self._cache[key] = value return value def _ResolveVariables(self, value: str) -> str: if value == "": return "" elif "$" not in value: return value rawValue = value result = "" while (len(rawValue) > 0): # print(f"_ResolveVariables: LOOP rawValue='{rawValue}'") beginPos = rawValue.find("$") if beginPos < 0: result += rawValue rawValue = "" else: result += rawValue[:beginPos] if rawValue[beginPos + 1] == "$": result += "$" rawValue = rawValue[1:] elif rawValue[beginPos + 1] == "{": endPos = rawValue.find("}", beginPos) nextPos = rawValue.rfind("$", beginPos, endPos) if endPos < 0: raise Exception(f"") # XXX: InterpolationSyntaxError(option, section, f"Bad interpolation variable reference {rest!r}") if (nextPos > 0) and (nextPos < endPos): # an embedded $-sign path = rawValue[nextPos+2:endPos] # print(f"_ResolveVariables: path='{path}'") innervalue = self._GetValueByPathExpression(self._ToPath(path)) # print(f"_ResolveVariables: innervalue='{innervalue}'") rawValue = rawValue[beginPos:nextPos] + str(innervalue) + rawValue[endPos + 1:] # print(f"_ResolveVariables: new rawValue='{rawValue}'") else: path = rawValue[beginPos+2:endPos] rawValue = rawValue[endPos+1:] result += str(self._GetValueByPathExpression(self._ToPath(path))) return result def _GetValueByPathExpression(self, path: List[KeyT]) -> ValueT: node = self for p in path: if p == "..": node = node._parent else: node = node._GetNodeOrValue(p) if isinstance(node, Dictionary): raise Exception(f"Error when resolving path expression '{':'.join(path)}' at '{p}'.") from TypeError(f"") # XXX: needs error messages return node def _GetNodeOrValueByPathExpression(self, path: List[KeyT]) -> ValueT: node = self for p in path: if p == "..": node = node._parent else: node = node._GetNodeOrValue(p) return node @export class Dictionary(Node, Abstract_Dict): """A dictionary node in a YAML data file.""" _keys: List[KeyT] #: List of keys in this dictionary. def __init__( self, root: "Configuration", parent: NodeT, key: KeyT, yamlNode: CommentedMap ) -> None: """ Initializes a YAML dictionary. :param root: Reference to the root node. :param parent: Reference to the parent node. :param key: :param yamlNode: Reference to the YAML node. """ Node.__init__(self, root, parent, key, yamlNode) self._keys = [str(k) for k in yamlNode.keys()] def __contains__(self, key: KeyT) -> bool: """ Checks if the key is in this dictionary. :param key: The key to check. :returns: ``True``, if the key is in the dictionary. """ return key in self._keys def __iter__(self) -> typing_Iterator[ValueT]: """ Returns an iterator to iterate dictionary keys. :returns: Dictionary key iterator. """ class Iterator(metaclass=ExtendedType, slots=True): """Iterator to iterate dictionary items.""" _iter: typing_Iterator[ValueT] _obj: Dictionary def __init__(self, obj: Dictionary) -> None: """ Initializes an iterator for a YAML dictionary node. :param obj: YAML dictionary to iterate. """ self._iter = iter(obj._keys) self._obj = obj def __iter__(self) -> Self: """ Return itself to fulfil the iterator protocol. :returns: Itself. """ return self # pragma: no cover def __next__(self) -> ValueT: """ Returns the next item in the dictionary. :returns: Next item. """ key = next(self._iter) return self._obj[key] return Iterator(self) @export class Sequence(Node, Abstract_Seq): """A sequence node (ordered list) in a YAML data file.""" def __init__( self, root: "Configuration", parent: NodeT, key: KeyT, yamlNode: CommentedSeq ) -> None: """ Initializes a YAML sequence (list). :param root: Reference to the root node. :param parent: Reference to the parent node. :param key: :param yamlNode: Reference to the YAML node. """ Node.__init__(self, root, parent, key, yamlNode) self._length = len(yamlNode) def __iter__(self) -> typing_Iterator[ValueT]: """ Returns an iterator to iterate items in the sequence of sub-nodes. :returns: Iterator to iterate items in a sequence. """ class Iterator(metaclass=ExtendedType, slots=True): """Iterator to iterate sequence items.""" _i: int #: internal iterator position _obj: Sequence #: Sequence object to iterate def __init__(self, obj: Sequence) -> None: """ Initializes an iterator for a YAML sequence node. :param obj: YAML sequence to iterate. """ self._i = 0 self._obj = obj def __iter__(self) -> Self: """ Return itself to fulfil the iterator protocol. :returns: Itself. """ return self # pragma: no cover def __next__(self) -> ValueT: """ Returns the next item in the sequence. :returns: Next item. :raises StopIteration: If end of sequence is reached. """ try: result = self._obj[str(self._i)] self._i += 1 return result except IndexError: raise StopIteration return Iterator(self) setattr(Node, "DICT_TYPE", Dictionary) setattr(Node, "SEQ_TYPE", Sequence) @export class Configuration(Dictionary, Abstract_Configuration): """A configuration read from a YAML file.""" _yamlConfig: YAML def __init__(self, configFile: Path) -> None: """ Initializes a configuration instance that reads a YAML file as input. All sequence items or dictionaries key-value-pairs in the YAML file are accessible via Python's dictionary syntax. :param configFile: Configuration file to read and parse. """ if not configFile.exists(): raise ConfigurationException(f"JSON configuration file '{configFile}' not found.") from FileNotFoundError(configFile) with configFile.open("r", encoding="utf-8") as file: self._yamlConfig = YAML().load(file) Dictionary.__init__(self, self, self, None, self._yamlConfig) Abstract_Configuration.__init__(self, configFile) def __getitem__(self, key: str) -> ValueT: """ Access a configuration node by key. :param key: The key to look for. :returns: A node (sequence or dictionary) or scalar value (int, float, str). """ return self._GetNodeOrValue(str(key)) def __setitem__(self, key: str, value: ValueT) -> None: raise NotImplementedError() pyTooling-8.11.0/pyTooling/Configuration/__init__.py000066400000000000000000000206631513317154500225050ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ __ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Abstract configuration reader. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from pathlib import Path from typing import Union, ClassVar, Iterator, Type, Optional as Nullable try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType, mixin from pyTooling.Exceptions import ToolingException except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Configuration] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, mixin from Exceptions import ToolingException except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Configuration] Could not import directly!") raise ex __all__ = ["KeyT", "NodeT", "ValueT"] KeyT = Union[str, int] #: Type variable for keys. NodeT = Union["Dictionary", "Sequence"] #: Type variable for nodes. ValueT = Union[NodeT, str, int, float] #: Type variable for values. @export class ConfigurationException(ToolingException): """Base-exception of all exceptions raised by :mod:`pyTooling.Configuration`.""" @export class Node(metaclass=ExtendedType, slots=True): """Abstract node in a configuration data structure.""" DICT_TYPE: ClassVar[Type["Dictionary"]] #: Type reference used when instantiating new dictionaries SEQ_TYPE: ClassVar[Type["Sequence"]] #: Type reference used when instantiating new sequences _root: "Configuration" #: Reference to the root node. _parent: "Dictionary" #: Reference to a parent node. def __init__(self, root: "Configuration" = None, parent: Nullable[NodeT] = None) -> None: """ Initializes a node. :param root: Reference to the root node. :param parent: Reference to the parent node. """ self._root = root self._parent = parent def __len__(self) -> int: # type: ignore[empty-body] """ Returns the number of sub-elements. :returns: Number of sub-elements. """ def __getitem__(self, key: KeyT) -> ValueT: # type: ignore[empty-body] """ Access an element in the node by index or key. :param key: Index or key of the element. :returns: A node (sequence or dictionary) or scalar value (int, float, str). """ raise NotImplementedError() def __setitem__(self, key: KeyT, value: ValueT) -> None: # type: ignore[empty-body] """ Set an element in the node by index or key. :param key: Index or key of the element. :param value: Value to set """ raise NotImplementedError() def __iter__(self) -> Iterator[ValueT]: # type: ignore[empty-body] """ Returns an iterator to iterate a node. :returns: Node iterator. """ raise NotImplementedError() @property def Key(self) -> KeyT: raise NotImplementedError() @Key.setter def Key(self, value: KeyT) -> None: raise NotImplementedError() def QueryPath(self, query: str) -> ValueT: # type: ignore[empty-body] """ Return a node or value based on a path description to that node or value. :param query: String describing the path to the node or value. :returns: A node (sequence or dictionary) or scalar value (int, float, str). """ raise NotImplementedError() @export @mixin class Dictionary(Node): """Abstract dictionary node in a configuration.""" def __init__(self, root: "Configuration" = None, parent: Nullable[NodeT] = None) -> None: """ Initializes a dictionary. :param root: Reference to the root node. :param parent: Reference to the parent node. """ Node.__init__(self, root, parent) def __contains__(self, key: KeyT) -> bool: # type: ignore[empty-body] raise NotImplementedError() @export @mixin class Sequence(Node): """Abstract sequence node in a configuration.""" def __init__(self, root: "Configuration" = None, parent: Nullable[NodeT] = None) -> None: """ Initializes a sequence. :param root: Reference to the root node. :param parent: Reference to the parent node. """ Node.__init__(self, root, parent) def __getitem__(self, index: int) -> ValueT: # type: ignore[empty-body] raise NotImplementedError() def __setitem__(self, index: int, value: ValueT) -> None: # type: ignore[empty-body] raise NotImplementedError() setattr(Node, "DICT_TYPE", Dictionary) setattr(Node, "SEQ_TYPE", Sequence) @export @mixin class Configuration(Node): """Abstract root node in a configuration.""" _configFile: Path #: Path to the configuration file. def __init__(self, configFile: Path, root: "Configuration" = None, parent: Nullable[NodeT] = None) -> None: """ Initializes a configuration. :param configFile: Configuration file. :param root: Reference to the root node. :param parent: Reference to the parent node. """ Node.__init__(self, root, parent) self._configFile = configFile @readonly def ConfigFile(self) -> Path: """ Read-only property to access the configuration file's path. :returns: Path to the configuration file. """ return self._configFile pyTooling-8.11.0/pyTooling/Decorators/000077500000000000000000000000001513317154500176635ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Decorators/__init__.py000066400000000000000000000260031513317154500217750ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ ___ ___ _ __ __ _| |_ ___ _ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \/ __/ _ \| '__/ _` | __/ _ \| '__/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ (_| (_) | | | (_| | || (_) | | \__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___|\___\___/|_| \__,_|\__\___/|_| |___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Decorators controlling visibility of entities in a Python module. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ import sys from functools import wraps from types import FunctionType from typing import Union, Type, TypeVar, Callable, Any, Optional as Nullable __all__ = ["export", "Param", "RetType", "Func", "T"] try: # See https://stackoverflow.com/questions/47060133/python-3-type-hinting-for-decorator from typing import ParamSpec # WORKAROUND: exists since Python 3.10 Param = ParamSpec("Param") #: A parameter specification for function or method RetType = TypeVar("RetType") #: Type variable for a return type Func = Callable[Param, RetType] #: Type specification for a function except ImportError: # pragma: no cover Param = ... #: A parameter specification for function or method RetType = TypeVar("RetType") #: Type variable for a return type Func = Callable[..., RetType] #: Type specification for a function T = TypeVar("T", bound=Union[Type, FunctionType]) #: A type variable for a classes or functions. C = TypeVar("C", bound=Callable) #: A type variable for functions or methods. def export(entity: T) -> T: """ Register the given function or class as publicly accessible in a module. Creates or updates the ``__all__`` attribute in the module in which the decorated entity is defined to include the name of the decorated entity. +---------------------------------------------+------------------------------------------------+ | ``to_export.py`` | ``another_file.py`` | +=============================================+================================================+ | .. code-block:: python | .. code-block:: python | | | | | from pyTooling.Decorators import export | from .to_export import * | | | | | @export | | | def exported(): | # 'exported' will be listed in __all__ | | pass | assert "exported" in globals() | | | | | def not_exported(): | # 'not_exported' won't be listed in __all__ | | pass | assert "not_exported" not in globals() | | | | +---------------------------------------------+------------------------------------------------+ :param entity: The function or class to include in `__all__`. :returns: The unmodified function or class. :raises AttributeError: If parameter ``entity`` has no ``__module__`` member. :raises TypeError: If parameter ``entity`` is not a top-level entity in a module. :raises TypeError: If parameter ``entity`` has no ``__name__``. """ # * Based on an idea by Duncan Booth: # http://groups.google.com/group/comp.lang.python/msg/11cbb03e09611b8a # * Improved via a suggestion by Dave Angel: # http://groups.google.com/group/comp.lang.python/msg/3d400fb22d8a42e1 if not hasattr(entity, "__module__"): raise AttributeError(f"{entity} has no __module__ attribute. Please ensure it is a top-level function or class reference defined in a module.") if hasattr(entity, "__qualname__"): if any(i in entity.__qualname__ for i in (".", "", "")): raise TypeError(f"Only named top-level functions and classes may be exported, not {entity}") if not hasattr(entity, "__name__") or entity.__name__ == "": raise TypeError(f"Entity must be a named top-level function or class, not {entity.__class__}") try: module = sys.modules[entity.__module__] except KeyError: raise ValueError(f"Module {entity.__module__} is not present in sys.modules. Please ensure it is in the import path before calling export().") if hasattr(module, "__all__"): if entity.__name__ not in module.__all__: # type: ignore module.__all__.append(entity.__name__) # type: ignore else: module.__all__ = [entity.__name__] # type: ignore return entity @export def notimplemented(message: str) -> Callable: """ Mark a method as *not implemented* and replace the implementation with a new method raising a :exc:`NotImplementedError`. The original method is stored in ``.__wrapped__`` and it's doc-string is copied to the replacing method. In additional the field ``.__notImplemented__`` is added. .. admonition:: ``example.py`` .. code-block:: python class Data: @notimplemented def method(self) -> bool: '''This method needs to be implemented''' return True :param method: Method that is marked as *not implemented*. :returns: Replacement method, which raises a :exc:`NotImplementedError`. .. seealso:: * :func:`~pyTooling.Metaclasses.abstractmethod` * :func:`~pyTooling.Metaclasses.mustoverride` """ def decorator(method: C) -> C: @wraps(method) def func(*_, **__): raise NotImplementedError(message) func.__notImplemented__ = True return func return decorator @export def readonly(func: Callable) -> property: """ Marks a property as *read-only*. The doc-string will be taken from the getter-function. It will remove ``.setter`` and ``.deleter`` from the property descriptor. :param func: Function to convert to a read-only property. :returns: A property object with just a getter. .. seealso:: :class:`property` A decorator to convert getter, setter and deleter methods into a property applying the descriptor protocol. """ prop = property(fget=func, fset=None, fdel=None, doc=func.__doc__) return prop @export def InheritDocString(baseClass: type, merge: bool = False) -> Callable[[Union[Func, type]], Union[Func, type]]: """ Copy the doc-string from given base-class to the method this decorator is applied to. .. admonition:: ``example.py`` .. code-block:: python from pyTooling.Decorators import InheritDocString class Class1: def method(self): '''Method's doc-string.''' class Class2(Class1): @InheritDocString(Class1) def method(self): super().method() :param baseClass: Base-class to copy the doc-string from to the new method being decorated. :returns: Decorator function that copies the doc-string. """ def decorator(param: Union[Func, type]) -> Union[Func, type]: """ Decorator function, which copies the doc-string from base-class' method to method ``m``. :param param: Method to which the doc-string from a method in ``baseClass`` (with same className) should be copied. :returns: Same method, but with overwritten doc-string field (``__doc__``). """ if isinstance(param, type): baseDoc = baseClass.__doc__ elif callable(param): baseDoc = getattr(baseClass, param.__name__).__doc__ else: return param if merge: if param.__doc__ is None: param.__doc__ = baseDoc elif baseDoc is not None: param.__doc__ = baseDoc + "\n\n" + param.__doc__ else: param.__doc__ = baseDoc return param return decorator pyTooling-8.11.0/pyTooling/Dependency/000077500000000000000000000000001513317154500176345ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Dependency/Python.py000066400000000000000000000461711513317154500215000ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \ '_ \ / _ \ '_ \ / _` |/ _ \ '_ \ / __| | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, | # # |_| |___/ |___/ |_| |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Implementation of package dependencies. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from asyncio import run as asyncio_run, gather as asyncio_gather from datetime import datetime from enum import IntEnum from functools import wraps, update_wrapper from threading import RLock from typing import Optional as Nullable, List, Dict, Union, Iterable, Mapping, Callable, Iterator try: from aiohttp import ClientSession except ImportError as ex: # pragma: no cover raise Exception(f"Optional dependency 'aiohttp' not installed. Either install pyTooling with extra dependencies 'pyTooling[pypi]' or install 'aiohttp' directly.") from ex try: from packaging.requirements import Requirement except ImportError as ex: # pragma: no cover raise Exception(f"Optional dependency 'packaging' not installed. Either install pyTooling with extra dependencies 'pyTooling[pypi]' or install 'packaging' directly.") from ex try: from requests import Session, HTTPError except ImportError as ex: # pragma: no cover raise Exception(f"Optional dependency 'requests' not installed. Either install pyTooling with extra dependencies 'pyTooling[pypi]' or install 'requests' directly.") from ex try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride from pyTooling.Exceptions import ToolingException from pyTooling.Common import getFullyQualifiedName, firstKey, firstValue from pyTooling.Dependency import Package, PackageStorage, PackageVersion, PackageDependencyGraph from pyTooling.GenericPath.URL import URL from pyTooling.Versioning import SemanticVersion, PythonVersion, Parts except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Dependency] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, abstractmethod, mustoverride from Exceptions import ToolingException from Common import getFullyQualifiedName, firstKey, firstValue from Dependency import Package, PackageStorage, PackageVersion, PackageDependencyGraph from GenericPath.URL import URL from Versioning import SemanticVersion, PythonVersion, Parts except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Dependency] Could not import directly!") raise ex @export class LazyLoaderState(IntEnum): Uninitialized = 0 #: No data or minimal data like ID or name. Initialized = 1 #: Initialized by some __init__ parameters. PartiallyLoaded = 2 #: Some additional data was loaded. FullyLoaded = 3 #: All data is loaded. PostProcessed = 4 #: Loaded data triggered further processing. @export class lazy: """ Unified decorator that supports: 1. @lazy(state) def method() 2. @lazy(state) @property def prop() """ def __init__(self, _requiredState: LazyLoaderState = LazyLoaderState.PartiallyLoaded): self._requiredState = _requiredState self._wrapped = None def __call__(self, wrapped): self._wrapped = wrapped # If it's a function, we update metadata. # If it's a property, it doesn't support update_wrapper directly. if hasattr(wrapped, "__name__"): update_wrapper(self, wrapped) return self def __get__(self, obj, objtype=None): if obj is None: return self # 1. Thread-safe state check with obj.__lazy_lock__: if obj.__lazy_state__ < self._requiredState: obj.__lazy_loader__(self._requiredState) # 2. Determine if we are wrapping a property or a method if isinstance(self._wrapped, property): # If it's a property, call its __get__ to return the value return self._wrapped.__get__(obj, objtype) # 3. Otherwise, treat as a method and return a bound wrapper @wraps(self._wrapped) def wrapper(*args, **kwargs): return self._wrapped(obj, *args, **kwargs) return wrapper @export class LazyLoadableMixin(metaclass=ExtendedType, mixin=True): __lazy_state__: LazyLoaderState __lazy_lock__: RLock def __init__(self, targetLevel: LazyLoaderState = LazyLoaderState.Initialized) -> None: self.__lazy_state__ = LazyLoaderState.Initialized self.__lazy_lock__ = RLock() if targetLevel > self.__lazy_state__: with self.__lazy_lock__: self.__lazy_loader__(targetLevel) @abstractmethod def __lazy_loader__(self, targetLevel: LazyLoaderState) -> None: pass @export class Distribution(metaclass=ExtendedType, slots=True): _filename: str _url: URL _uploadTime: datetime def __init__(self, filename: str, url: Union[str, URL], uploadTime: datetime) -> None: if not isinstance(filename, str): ex = TypeError("Parameter 'filename' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(filename)}'.") raise ex self._filename = filename if isinstance(url, str): url = URL.Parse(url) elif not isinstance(url, URL): ex = TypeError("Parameter 'url' is not of type 'URL'.") ex.add_note(f"Got type '{getFullyQualifiedName(url)}'.") raise ex self._url = url if not isinstance(uploadTime, datetime): ex = TypeError("Parameter 'uploadTime' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(uploadTime)}'.") raise ex self._uploadTime = uploadTime @readonly def Filename(self) -> str: return self._filename @readonly def URL(self) -> URL: return self._url @readonly def UploadTime(self) -> datetime: return self._uploadTime def __repr__(self) -> str: return f"Distribution: {self._filename}" def __str__(self) -> str: return f"{self._filename}" @export class Release(PackageVersion, LazyLoadableMixin): _files: List[Distribution] _requirements: Dict[Union[str, None], List[Requirement]] _api: Nullable[URL] _session: Nullable[Session] def __init__( self, version: PythonVersion, timestamp: datetime, files: Nullable[Iterable[Distribution]] = None, requirements: Nullable[Mapping[str, List[Requirement]]] = None, project: Nullable["Project"] = None, lazy: LazyLoaderState = LazyLoaderState.Initialized ) -> None: if project is not None and (storage := project._storage) is not None: self._api = storage._api self._session = storage._session else: self._api = None self._session = None super().__init__(version, project, timestamp) LazyLoadableMixin.__init__(self, lazy) self._files = [file for file in files] if files is not None else [] self._requirements = {k: v for k, v in requirements} if requirements is not None else {None: []} def __lazy_loader__(self, targetLevel: LazyLoaderState) -> None: if targetLevel >= LazyLoaderState.PartiallyLoaded: self.DownloadDetails() if targetLevel >= LazyLoaderState.PostProcessed: self.PostProcess() @lazy(LazyLoaderState.PostProcessed) @PackageVersion.DependsOn.getter def DependsOn(self) -> Dict["Package", Dict[SemanticVersion, "PackageVersion"]]: return super().DependsOn @readonly def Project(self) -> "Project": return self._package @lazy(LazyLoaderState.PartiallyLoaded) @readonly def Files(self) -> List[Distribution]: return self._files @lazy(LazyLoaderState.PartiallyLoaded) @readonly def Requirements(self) -> Dict[str, List[Requirement]]: return self._requirements def _GetPyPIEndpoint(self) -> str: return f"{self._package._name.lower()}/{self._version}/json" def DownloadDetails(self) -> None: if self._session is None: # TODO: NoSessionAvailableException raise ToolingException(f"No session available.") response = self._session.get(url=f"{self._api}{self._GetPyPIEndpoint()}") try: response.raise_for_status() except HTTPError as ex: if ex.response.status_code == 404: # TODO: ReleaseNotFoundException raise ToolingException(f"Release '{self._version}' of package '{self._package._name}' not found.") self.UpdateDetailsFromPyPIJSON(response.json()) index: PythonPackageIndex = self._package._storage for requirement in self._requirements[None]: packageName = requirement.name index.DownloadProject(packageName, True) def UpdateDetailsFromPyPIJSON(self, json) -> None: infoNode = json["info"] if (extras := infoNode["provides_extra"]) is not None: self._requirements = {extra: [] for extra in extras} self._requirements[None] = [] if (requirements := infoNode["requires_dist"]) is not None: brokenRequirements = [] for requirement in requirements: req = Requirement(requirement) # Handle requirements without an extra marker if req.marker is None: self._requirements[None].append(req) continue for extra in self._requirements.keys(): if extra is not None and req.marker.evaluate({"extra": extra}): self._requirements[extra].append(req) break else: brokenRequirements.append(req) # TODO: raise a warning if len(brokenRequirements) > 0: self._requirements[0] = brokenRequirements self.__lazy_state__ = LazyLoaderState.FullyLoaded def PostProcess(self) -> None: index: PythonPackageIndex = self._package._storage for requirement in self._requirements[None]: package = index.DownloadProject(requirement.name) for release in package: if str(release._version) in requirement.specifier: self.AddDependencyToPackageVersion(release) self.SortDependencies() self.__lazy_state__ = LazyLoaderState.PostProcessed @lazy(LazyLoaderState.PartiallyLoaded) def __repr__(self) -> str: return f"Release: {self._package._name}:{self._version} Files: {len(self._files)}" def __str__(self) -> str: return f"{self._version}" @export class Project(Package, LazyLoadableMixin): _url: Nullable[URL] _api: Nullable[URL] _session: Nullable[Session] def __init__( self, name: str, url: Union[str, URL], releases: Nullable[Iterable[Release]] = None, index: Nullable["PythonPackageIndex"] = None, lazy: LazyLoaderState = LazyLoaderState.Initialized ) -> None: if index is not None: self._api = index._api self._session = index._session else: self._api = None self._session = None super().__init__(name, storage=index) LazyLoadableMixin.__init__(self, lazy) # if isinstance(url, str): # url = URL.Parse(url) # elif not isinstance(url, URL): # ex = TypeError("Parameter 'url' is not of type 'URL'.") # ex.add_note(f"Got type '{getFullyQualifiedName(url)}'.") # raise ex # # self._url = url # self._releases = {release.Version: release for release in sorted(releases, key=lambda r: r.Version)} if releases is not None else {} def __lazy_loader__(self, targetLevel: LazyLoaderState) -> None: if targetLevel >= LazyLoaderState.PartiallyLoaded: self.DownloadDetails() if targetLevel >= LazyLoaderState.PostProcessed: self.DownloadReleaseDetails() @readonly def PackageIndex(self) -> "PythonPackageIndex": return self._storage @lazy(LazyLoaderState.PartiallyLoaded) @readonly def URL(self) -> URL: return self._url @lazy(LazyLoaderState.PartiallyLoaded) @readonly def Releases(self) -> Dict[PythonVersion, Release]: return self._versions @lazy(LazyLoaderState.PartiallyLoaded) @readonly def ReleaseCount(self) -> int: return len(self._versions) @lazy(LazyLoaderState.PartiallyLoaded) @readonly def LatestRelease(self) -> Release: return firstValue(self._versions) def _GetPyPIEndpoint(self) -> str: return f"{self._name.lower()}/json" def DownloadDetails(self) -> None: if self._session is None: # TODO: NoSessionAvailableException raise ToolingException(f"No session available.") response = self._session.get(url=f"{self._api}{self._GetPyPIEndpoint()}") try: response.raise_for_status() except HTTPError as ex: if ex.response.status_code == 404: # TODO: ReleaseNotFoundException raise ToolingException(f"Package '{self._name}' not found.") self.UpdateDetailsFromPyPIJSON(response.json()) def UpdateDetailsFromPyPIJSON(self, json) -> None: infoNode = json["info"] releasesNode = json["releases"] # Update project/package URL self._url = URL.Parse(infoNode["project_url"]) # Convert key to Version number, skip empty releases convertedReleasesNode = {} for k, v in releasesNode.items(): if len(v) == 0: continue try: version = PythonVersion.Parse(k) convertedReleasesNode[version] = v except ValueError as ex: print(f"Unsupported version format '{k}' - {ex}") for version, releaseNode in sorted(convertedReleasesNode.items(), key=lambda t: t[0]): if Parts.Postfix in version._parts: pass files = [Distribution(file["filename"], file["url"], datetime.fromisoformat(file["upload_time_iso_8601"]), ) for file in releaseNode] lazy = LazyLoaderState.PartiallyLoaded if LazyLoaderState.PartiallyLoaded <= self.__lazy_state__ <= LazyLoaderState.FullyLoaded else LazyLoaderState.Initialized Release( version, files[0]._uploadTime, files, project=self, lazy=lazy ) self.SortVersions() self.__lazy_state__ = LazyLoaderState.FullyLoaded def DownloadReleaseDetails(self) -> None: async def ParallelDownloadReleaseDetails(): async def routine(session, release: Release): if Parts.Postfix in release._version._parts: pass async with session.get(self._GetPyPIEndpoint()) as response: json = await response.json() response.raise_for_status() release.UpdateDetailsFromPyPIJSON(json) async with ClientSession(base_url=str(self._api), headers={"accept": "application/json"}) as session: tasks = [] for release in self._versions.values(): # type: Release tasks.append(routine(session, release)) results = await asyncio_gather(*tasks, return_exceptions=True) delList = [] for release, result in zip(self.Releases.values(), results): if isinstance(result, Exception): delList.append((release, result)) # TODO: raise a warning for release, ex in delList: print(f" Removing {release.Project._name} {release.Version} - {ex}") del self.Releases[release.Version] asyncio_run(ParallelDownloadReleaseDetails()) self.__lazy_state__ = LazyLoaderState.PostProcessed def __repr__(self) -> str: return f"Project: {self._name} latest: {self.LatestRelease._version}" def __str__(self) -> str: return f"{self._name}" @export class PythonPackageIndex(PackageStorage): _url: URL _api: URL _session: Session def __init__(self, name: str, url: Union[str, URL], api: Union[str, URL], graph: "PackageDependencyGraph") -> None: super().__init__(name, graph) if isinstance(url, str): url = URL.Parse(url) elif not isinstance(url, URL): ex = TypeError("Parameter 'url' is not of type 'URL'.") ex.add_note(f"Got type '{getFullyQualifiedName(url)}'.") raise ex self._url = url if isinstance(api, str): api = URL.Parse(api) elif not isinstance(api, URL): ex = TypeError("Parameter 'api' is not of type 'URL'.") ex.add_note(f"Got type '{getFullyQualifiedName(api)}'.") raise ex self._api = api self._session = Session() self._session.headers["accept"] = "application/json" @readonly def URL(self) -> URL: return self._url @readonly def API(self) -> URL: return self._api @readonly def Projects(self) -> Dict[str, Project]: return self._packages @readonly def ProjectCount(self) -> int: return len(self._packages) def _GetPyPIEndpoint(self, projectName: str) -> str: return f"{self._api}{projectName.lower()}/json" def DownloadProject(self, projectName: str, lazy: LazyLoaderState = LazyLoaderState.PartiallyLoaded) -> Project: project = Project(projectName, "", index=self, lazy=lazy) return project def __repr__(self) -> str: return f"{self._name}" def __str__(self) -> str: return f"{self._name}" @export class PythonPackageDependencyGraph(PackageDependencyGraph): def __init__(self, name: str) -> None: super().__init__(name) pyTooling-8.11.0/pyTooling/Dependency/__init__.py000066400000000000000000000617711513317154500217610ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \ '_ \ / _ \ '_ \ / _` |/ _ \ '_ \ / __| | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, | # # |_| |___/ |___/ |_| |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Implementation of package dependencies. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from datetime import datetime from typing import Optional as Nullable, Dict, Union, Iterable, Set, Self, Iterator try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride from pyTooling.Exceptions import ToolingException from pyTooling.Common import getFullyQualifiedName, firstKey from pyTooling.Versioning import SemanticVersion except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Dependency] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, abstractmethod, mustoverride from Exceptions import ToolingException from Common import getFullyQualifiedName, firstKey from Versioning import SemanticVersion except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Dependency] Could not import directly!") raise ex @export class PackageVersion(metaclass=ExtendedType, slots=True): """ The package's version of a :class:`Package`. A :class:`Package` has multiple available versions. A version can have multiple dependencies to other :class:`PackageVersion`s. """ _package: "Package" #: Reference to the corresponding package _version: SemanticVersion #: :class:`SemanticVersion` of this package version. _releasedAt: Nullable[datetime] _dependsOn: Dict["Package", Dict[SemanticVersion, "PackageVersion"]] #: Versioned dependencies to other packages. def __init__(self, version: SemanticVersion, package: "Package", releasedAt: Nullable[datetime] = None) -> None: """ Initializes a package version. :param version: Semantic version of this package. :param package: Package this version is associated to. :param releasedAt: Optional release date and time. :raises TypeError: When parameter 'version' is not of type 'SemanticVersion'. :raises TypeError: When parameter 'package' is not of type 'Package'. :raises TypeError: When parameter 'releasedAt' is not of type 'datetime'. :raises ToolingException: When version already exists for the associated package. """ if not isinstance(version, SemanticVersion): ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.") ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") raise ex elif version in package._versions: raise ToolingException(f"Version '{version}' is already registered in package '{package._name}'.") self._version = version package._versions[version] = self if not isinstance(package, Package): ex = TypeError("Parameter 'package' is not of type 'Package'.") ex.add_note(f"Got type '{getFullyQualifiedName(package)}'.") raise ex self._package = package if releasedAt is not None and not isinstance(releasedAt, datetime): ex = TypeError("Parameter 'releasedAt' is not of type 'datetime'.") ex.add_note(f"Got type '{getFullyQualifiedName(releasedAt)}'.") raise ex self._releasedAt = releasedAt self._dependsOn = {} @readonly def Package(self) -> "Package": """ Read-only property to access the associated package. :returns: Associated package. """ return self._package @readonly def Version(self) -> SemanticVersion: """ Read-only property to access the semantic version of a package. :returns: Semantic version of a package. """ return self._version @readonly def ReleasedAt(self) -> Nullable[datetime]: """ Read-only property to access the release date and time. :returns: Optional release date and time. """ return self._releasedAt @readonly def DependsOn(self) -> Dict["Package", Dict[SemanticVersion, "PackageVersion"]]: """ Read-only property to access the dictionary of dictionaries referencing dependencies. The outer dictionary key groups dependencies by :class:`Package`. |br| The inner dictionary key accesses dependencies by :class:`~pyTooling.Versioning.SemanticVersion`. :returns: Dictionary of dependencies. """ return self._dependsOn def AddDependencyToPackageVersion(self, packageVersion: "PackageVersion") -> None: """ Add a dependency from current package version to another package version. :param packageVersion: Dependency to be added. """ if (package := packageVersion._package) in self._dependsOn: pack = self._dependsOn[package] if (version := packageVersion._version) in pack: pass else: pack[version] = packageVersion else: self._dependsOn[package] = {packageVersion._version: packageVersion} def AddDependencyToPackageVersions(self, packageVersions: Iterable["PackageVersion"]) -> None: """ Add multiple dependencies from current package version to a list of other package versions. :param packageVersions: Dependencies to be added. """ # TODO: check for iterable for packageVersion in packageVersions: if (package := packageVersion._package) in self._dependsOn: pack = self._dependsOn[package] if (version := packageVersion._version) in pack: pass else: pack[version] = packageVersion else: self._dependsOn[package] = {packageVersion._version: packageVersion} def AddDependencyTo( self, package: Union[str, Package], version: Union[str, SemanticVersion, Iterable[Union[str, SemanticVersion]]] ) -> None: """ Add a dependency from current package version to another package version. :param package: :class:`Package` object or name of the package. :param version: :class:`~pyTooling.Versioning.SemanticVersion` object or version string or an iterable thereof. :return: """ if isinstance(package, str): package = self._package._storage._packages[package] elif not isinstance(package, Package): ex = TypeError(f"Parameter 'package' is not of type 'str' nor 'Package'.") ex.add_note(f"Got type '{getFullyQualifiedName(package)}'.") raise ex if isinstance(version, str): version = SemanticVersion.Parse(version) elif isinstance(version, Iterable): for v in version: if isinstance(v, str): v = SemanticVersion.Parse(v) elif not isinstance(v, SemanticVersion): ex = TypeError(f"Parameter 'version' contains an element, which is not of type 'str' nor 'SemanticVersion'.") ex.add_note(f"Got type '{getFullyQualifiedName(v)}'.") raise ex# packageVersion = package._versions[v] self.AddDependencyToPackageVersion(packageVersion) return elif not isinstance(version, SemanticVersion): ex = TypeError(f"Parameter 'version' is not of type 'str' nor 'SemanticVersion'.") ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") raise ex packageVersion = package._versions[version] self.AddDependencyToPackageVersion(packageVersion) def SortDependencies(self) -> Self: """ Sort versions of a package and dependencies by version, thus dependency resolution can work on pre-sorted lists and dictionaries. :returns: The instance itself (for method-chaining). """ for package, versions in self._dependsOn.items(): self._dependsOn[package] = {version: versions[version] for version in sorted(versions.keys(), reverse=True)} return self def SolveLatest(self) -> Iterable["PackageVersion"]: """ Solve the dependency problem, while using preferably latest versions. .. todo:: Describe algorithm. :returns: A list of :class:`PackageVersion`s fulfilling the constraints of the dependency problem. :raises ToolingException: When there is no valid solution to the problem. """ solution: Dict["Package", "PackageVersion"] = {self._package: self} def _recursion(currentSolution: Dict["Package", "PackageVersion"]) -> bool: # 1. Identify all required packages based on current selection requiredPackages: Set["Package"] = set() for packageVersion in currentSolution.values(): requiredPackages.update(packageVersion.DependsOn.keys()) # 2. Identify which required packages are missing from the solution missingPackages = requiredPackages - currentSolution.keys() # Base Case: If no packages are missing, the graph is complete and valid if len(missingPackages) == 0: return True # 3. Pick the next package to resolve # (Heuristic: we just pick the first one, but could be optimized) targetPackage = next(iter(missingPackages)) # 4. Determine valid candidates # The candidate version must satisfy the constraints of all parents currently in the solution allowedVersions: Nullable[Set[SemanticVersion]] = None for parentPackageVersion in currentSolution.values(): if targetPackage in parentPackageVersion.DependsOn: # Get the set of versions allowed by this specific parent # (Keys of the inner dict are SemanticVersion objects) parentConstraints = set(parentPackageVersion.DependsOn[targetPackage].keys()) if allowedVersions is None: allowedVersions = parentConstraints else: # Intersect with existing constraints (must satisfy everyone) allowedVersions &= parentConstraints # If the intersection is empty, no version satisfies all parents -> backtrack if not allowedVersions: return False # 5. Try candidates (sorted descending to prioritize latest) # We convert the set to a list and sort it reverse for version_key in sorted(list(allowedVersions), reverse=True): candidate = targetPackage.Versions[version_key] # 6. Check compatibility (reverse dependencies) # Does the candidate depend on anything we have already selected? # If so, does the candidate accept the version we already picked? isCompatible = True for existingPackage, existingPackageVersion in currentSolution.items(): if existingPackage in candidate.DependsOn: # If candidate relies on 'existingPackage', check if 'existingPackageVersion' is in the allowed list if existingPackageVersion._version not in candidate.DependsOn[existingPackage]: isCompatible = False break if isCompatible: # Tentatively add to solution currentSolution[targetPackage] = candidate # Recurse if _recursion(currentSolution): return True # If recursion failed, remove (backtrack) and try next version del currentSolution[targetPackage] # If we run out of versions for this package, this path is dead return False # Run the solver if _recursion(solution): return list(solution.values()) else: raise ToolingException(f"Could not resolve dependencies for '{self}'.") def __len__(self) -> int: """ Returns the number of dependencies. :returns: Number of dependencies. """ return len(self._dependsOn) def __str__(self) -> str: """ Return a string representation of this package version. :returns: The package's name and version. """ return f"{self._package._name} - {self._version}" @export class Package(metaclass=ExtendedType, slots=True): """ The package, which exists in multiple versions (:class:`PackageVersion`). """ _storage: "PackageStorage" #: Reference to the package's storage. _name: str #: Name of the package. _versions: Dict[SemanticVersion, PackageVersion] #: A dictionary of available versions for this package. def __init__(self, name: str, *, storage: "PackageStorage") -> None: """ Initializes a package. :param name: Name of the package. :param storage: The package's storage. """ if not isinstance(name, str): ex = TypeError("Parameter 'name' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") raise ex self._name = name if not isinstance(storage, PackageStorage): ex = TypeError("Parameter 'storage' is not of type 'PackageStorage'.") ex.add_note(f"Got type '{getFullyQualifiedName(storage)}'.") raise ex self._storage = storage storage._packages[name] = self self._versions = {} @readonly def Storage(self) -> "PackageStorage": """ Read-only property to access the package's storage. :returns: Package storage. """ return self._storage @readonly def Name(self) -> str: """ Read-only property to access the package name. :returns: Name of the package. """ return self._name @readonly def Versions(self) -> Dict[SemanticVersion, PackageVersion]: """ Read-only property to access the dictionary of available versions. :returns: Available version dictionary. """ return self._versions @readonly def VersionCount(self) -> int: return len(self._versions) def SortVersions(self) -> None: """ Sort versions within this package in reverse order (latest first). """ self._versions = {k: self._versions[k].SortDependencies() for k in sorted(self._versions.keys(), reverse=True)} def __len__(self) -> int: """ Returns the number of available versions. :returns: Number of versions. """ return len(self._versions) def __iter__(self) -> Iterator[PackageVersion]: return iter(self._versions.values()) def __getitem__(self, version: Union[str, SemanticVersion]) -> PackageVersion: """ Access a package version in the package by version string or semantic version. :param version: Version as string or instance. :returns: The package version. :raises KeyError: If version is not available for the package. """ if isinstance(version, str): version = SemanticVersion.Parse(version) elif not isinstance(version, SemanticVersion): # TODO: raise proper type error raise TypeError() return self._versions[version] def __str__(self) -> str: """ Return a string representation of this package. :returns: The package's name and latest version. """ if len(self._versions) == 0: return f"{self._name} (empty)" else: return f"{self._name} (latest: {firstKey(self._versions)})" @export class PackageStorage(metaclass=ExtendedType, slots=True): """ A storage for packages. """ _graph: "PackageDependencyGraph" #: Reference to the overall dependency graph data structure. _name: str #: Package dependency graph name _packages: Dict[str, Package] #: Dictionary of known packages. def __init__(self, name: str, graph: "PackageDependencyGraph") -> None: """ Initializes the package storage. :param name: Name of the package storage. :param graph: PackageDependencyGraph instance (parent). """ if not isinstance(name, str): ex = TypeError("Parameter 'name' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") raise ex self._name = name if not isinstance(graph, PackageDependencyGraph): ex = TypeError("Parameter 'graph' is not of type 'PackageDependencyGraph'.") ex.add_note(f"Got type '{getFullyQualifiedName(graph)}'.") raise ex self._graph = graph graph._storages[name] = self self._packages = {} @readonly def Graph(self) -> "PackageDependencyGraph": """ Read-only property to access the package dependency graph. :returns: Package dependency graph. """ return self._graph @readonly def Name(self) -> str: """ Read-only property to access the package dependency graph's name. :returns: Name of the package dependency graph. """ return self._name @readonly def Packages(self) -> Dict[str, Package]: """ Read-only property to access the dictionary of known packages. :returns: Known packages dictionary. """ return self._packages @readonly def PackageCount(self) -> int: return len(self._packages) def CreatePackage(self, packageName: str) -> Package: """ Create a new package in the package dependency graph. :param packageName: Name of the new package. :returns: New package's instance. """ return Package(packageName, storage=self) def CreatePackages(self, packageNames: Iterable[str]) -> Iterable[Package]: """ Create multiple new packages in the package dependency graph. :param packageNames: List of package names. :returns: List of new package instances. """ return [Package(packageName, storage=self) for packageName in packageNames] def CreatePackageVersion(self, packageName: str, version: str) -> PackageVersion: """ Create a new package and a package version in the package dependency graph. :param packageName: Name of the new package. :param version: Version string. :returns: New package version instance. """ package = Package(packageName, storage=self) return PackageVersion(SemanticVersion.Parse(version), package) def CreatePackageVersions(self, packageName: str, versions: Iterable[str]) -> Iterable[PackageVersion]: """ Create a new package and multiple package versions in the package dependency graph. :param packageName: Name of the new package. :param versions: List of version string.s :returns: List of new package version instances. """ package = Package(packageName, storage=self) return [PackageVersion(SemanticVersion.Parse(version), package) for version in versions] def SortPackageVersions(self) -> None: """ Sort versions within all known packages in reverse order (latest first). """ for package in self._packages.values(): package.SortVersions() def __len__(self) -> int: """ Returns the number of known packages. :returns: Number of packages. """ return len(self._packages) def __iter__(self) -> Iterator[Package]: return iter(self._packages.values()) def __getitem__(self, name: str) -> Package: """ Access a known package in the package dependency graph by package name. :param name: Name of the package. :returns: The package. :raises KeyError: If package is not known within the package dependency graph. """ return self._packages[name] def __str__(self) -> str: """ Return a string representation of this graph. :returns: The graph's name and number of known packages. """ if len(self._packages) == 0: return f"{self._name} (empty)" else: return f"{self._name} ({len(self._packages)})" @export class PackageDependencyGraph(metaclass=ExtendedType, slots=True): """ A package dependency graph collecting all known packages. """ _name: str #: Package dependency graph name _storages: Dict[str, PackageStorage] #: Dictionary of known package storages. def __init__(self, name: str) -> None: """ Initializes the package dependency graph. :param name: Name of the dependency graph. """ if not isinstance(name, str): ex = TypeError("Parameter 'name' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") raise ex self._name = name self._storages = {} @readonly def Name(self) -> str: """ Read-only property to access the package dependency graph's name. :returns: Name of the package dependency graph. """ return self._name @readonly def Storages(self) -> Dict[str, PackageStorage]: """ Read-only property to access the dictionary of known package storages. :returns: Known package storage dictionary. """ return self._storages # def CreatePackage(self, packageName: str) -> Package: # """ # Create a new package in the package dependency graph. # # :param packageName: Name of the new package. # :returns: New package's instance. # """ # return Package(packageName, storage=self) # # def CreatePackages(self, packageNames: Iterable[str]) -> Iterable[Package]: # """ # Create multiple new packages in the package dependency graph. # # :param packageNames: List of package names. # :returns: List of new package instances. # """ # return [Package(packageName, storage=self) for packageName in packageNames] # # def CreatePackageVersion(self, packageName: str, version: str) -> PackageVersion: # """ # Create a new package and a package version in the package dependency graph. # # :param packageName: Name of the new package. # :param version: Version string. # :returns: New package version instance. # """ # package = Package(packageName, storage=self) # return PackageVersion(SemanticVersion.Parse(version), package) # # def CreatePackageVersions(self, packageName: str, versions: Iterable[str]) -> Iterable[PackageVersion]: # """ # Create a new package and multiple package versions in the package dependency graph. # # :param packageName: Name of the new package. # :param versions: List of version string.s # :returns: List of new package version instances. # """ # package = Package(packageName, storage=self) # return [PackageVersion(SemanticVersion.Parse(version), package) for version in versions] def SortPackageVersions(self) -> None: """ Sort versions within all known packages in reverse order (latest first). """ for storage in self._storages.values(): storage.SortPackageVersions() def __len__(self) -> int: """ Returns the number of known packages. :returns: Number of packages. """ return len(self._storages) def __iter__(self) -> Iterator[PackageStorage]: return iter(self._storages.values()) def __getitem__(self, name: str) -> PackageStorage: """ Access a known package storage in the package dependency graph by storage name. :param name: Name of the package storage. :returns: The package storage. :raises KeyError: If package storage is not known within the package dependency graph. """ return self._storages[name] def __str__(self) -> str: """ Return a string representation of this graph. :returns: The graph's name and number of known packages. """ count = sum(len(storage) for storage in self._storages.values()) if count == 0: return f"{self._name} (empty)" else: return f"{self._name} ({count})" pyTooling-8.11.0/pyTooling/Exceptions/000077500000000000000000000000001513317154500176775ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Exceptions/__init__.py000066400000000000000000000125141513317154500220130ustar00rootroot00000000000000# ==================================================================================================================== # # # # _____ _ _ _____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | ____|_ _____ ___ _ __ | |_(_) ___ _ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | _| \ \/ / __/ _ \ '_ \| __| |/ _ \| '_ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___ > < (_| __/ |_) | |_| | (_) | | | \__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____/_/\_\___\___| .__/ \__|_|\___/|_| |_|___/ # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ A common set of missing exceptions in Python. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ try: from pyTooling.Decorators import export except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Exceptions] Could not import from 'pyTooling.*'!") try: from Decorators import export except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Exceptions] Could not import from 'Decorators' directly!") raise ex @export class OverloadResolutionError(Exception): """ The exception is raised, when no matching overloaded method was found. .. seealso:: :func:`@overloadable ` |rarr| Mark a method as *overloadable*. """ @export class ExceptionBase(Exception): """Base exception derived from :exc:`Exception ` for all custom exceptions.""" def __init__(self, message: str = "") -> None: """ ExceptionBase initializer. :param message: The exception message. """ super().__init__() self.message = message def __str__(self) -> str: """Returns the exception's message text.""" return self.message # @DocumentMemberAttribute(False) # @MethodAlias(pyExceptions.with_traceback) # def with_traceback(self): pass @export class EnvironmentException(ExceptionBase): """The exception is raised when an expected environment variable is missing.""" @export class PlatformNotSupportedException(ExceptionBase): """The exception is raise if the platform is not supported.""" @export class NotConfiguredException(ExceptionBase): """The exception is raise if the requested setting is not configured.""" @export class ToolingException(Exception): """The exception is raised by pyTooling internal features.""" pyTooling-8.11.0/pyTooling/Filesystem/000077500000000000000000000000001513317154500177025ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Filesystem/__init__.py000066400000000000000000001031661513317154500220220ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | ___(_) | ___ ___ _ _ ___| |_ ___ _ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_ | | |/ _ \/ __| | | / __| __/ _ \ '_ ` _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| _| | | | __/\__ \ |_| \__ \ || __/ | | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|_|\___||___/\__, |___/\__\___|_| |_| |_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ An object-oriented file system abstraction for directory, file, symbolic link, ... statistics collection. .. important:: This isn't a replacement of :mod:`pathlib` introduced with Python 3.4. """ from os import scandir, readlink from enum import Enum from itertools import chain from pathlib import Path from typing import Optional as Nullable, Dict, Generic, Generator, TypeVar, List, Any, Callable, Union try: from pyTooling.Decorators import readonly, export from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType, abstractmethod from pyTooling.Common import getFullyQualifiedName, zipdicts from pyTooling.Stopwatch import Stopwatch from pyTooling.Tree import Node except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Filesystem] Could not import from 'pyTooling.*'!") try: from pyTooling.Decorators import readonly, export from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType, abstractmethod from pyTooling.Common import getFullyQualifiedName from pyTooling.Stopwatch import Stopwatch from pyTooling.Tree import Node except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Filesystem] Could not import directly!") raise ex __all__ = ["_ParentType"] _ParentType = TypeVar("_ParentType", bound="Element") """The type variable for a parent reference.""" @export class FilesystemException(ToolingException): """Base-exception of all exceptions raised by :mod:`pyTooling.Filesystem`.""" @export class NodeKind(Enum): """ Node kind for filesystem elements in a :ref:`tree `. This enumeration is used when converting the filesystem statistics tree to an instance of :mod:`pyTooling.Tree`. """ Directory = 0 #: Node represents a directory. File = 1 #: Node represents a regular file. SymbolicLink = 2 #: Node represents a symbolic link. @export class Base(metaclass=ExtendedType, slots=True): """ Base-class for all filesystem elements in :mod:`pyTooling.Filesystem`. It implements a size and a reference to the root element of the filesystem. """ _root: Nullable["Root"] #: Reference to the root of the filesystem statistics scope. _size: Nullable[int] #: Actual or aggregated size of the filesystem element. def __init__( self, size: Nullable[int], root: Nullable["Root"] ) -> None: """ Initialize the base-class with filesystem element size and root reference. :param size: Optional size of the element. :param root: Optional reference to the filesystem root element. """ if size is None: pass elif not isinstance(size, int): ex = TypeError("Parameter 'size' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(size)}'.") raise ex self._size = size self._root = root @property def Root(self) -> Nullable["Root"]: """ Property to access the root of the filesystem statistics scope. :returns: Root of the filesystem statistics scope. """ return self._root @Root.setter def Root(self, value: "Root") -> None: self._root = value @readonly def Size(self) -> int: """ Read-only property to access the element's size in Bytes. :returns: Size in Bytes. :raises FilesystemException: If size is not computed, yet. """ if self._size is None: raise FilesystemException("Size is not computed, yet.") return self._size # FIXME: @abstractmethod def ToTree(self) -> Node: """ Convert a filesystem element to a node in :mod:`pyTooling.Tree`. The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the filesystem element. Additional data will be stored in the node's key-value store. :returns: A tree's node referencing this filesystem element. """ raise NotImplementedError() @export class Element(Base, Generic[_ParentType]): """ Base-class for all named elements within a filesystem. It adds a name, parent reference and list of symbolic-link sources. .. hint:: Symbolic link sources are reverse references describing which symbolic links point to this element. """ _name: str #: Name of the filesystem element. _parent: _ParentType #: Reference to the filesystem element's parent (:class:`Directory`) _linkSources: List["SymbolicLink"] #: A list of symbolic links pointing to this filesystem element. def __init__( self, name: str, size: Nullable[int] = None, parent: Nullable[_ParentType] = None ) -> None: """ Initialize the element base-class with name, size and parent reference. :param name: Name of the element. :param size: Optional size of the element. :param parent: Optional parent reference. """ root = None # FIXME: if parent is None else parent._root super().__init__(size, root) self._parent = parent self._name = name self._linkSources = [] @property def Parent(self) -> _ParentType: """ Property to access the element's parent. :returns: Parent element. """ return self._parent @Parent.setter def Parent(self, value: _ParentType) -> None: self._parent = value if value._root is not None: self._root = value._root @readonly def Name(self) -> str: """ Read-only property to access the element's name. :returns: Element name. """ return self._name @readonly def Path(self) -> Path: raise NotImplemented(f"Property 'Path' is abstract.") def AddLinkSources(self, source: "SymbolicLink") -> None: """ Add a link source of a symbolic link to the named element (reverse reference). :param source: The referenced symbolic link. """ if not isinstance(source, SymbolicLink): ex = TypeError("Parameter 'source' is not of type 'SymbolicLink'.") ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.") raise ex self._linkSources.append(source) @export class Directory(Element["Directory"]): """ A **directory** represents a directory in the filesystem, which contains subdirectories, regular files and symbolic links. While scanning for subelements, the directory is populated with elements. Every file object added, gets registered in the filesystems :class:`Root` for deduplication. In case a file identifier already exists, the found filename will reference the same file objects. In turn, the file objects has then references to multiple filenames (parents). This allows to detect :term:`hardlinks `. The time needed for scanning the directory and its subelements is provided via :data:`ScanDuration`. After scnaning the directory for subelements, certain directory properties get aggregated. The time needed for aggregation is provided via :data:`AggregateDuration`. """ _path: Nullable[Path] #: Cached :class:`~pathlib.Path` object of this directory. _subdirectories: Dict[str, "Directory"] #: Dictionary containing name-:class:`Directory` pairs. _files: Dict[str, "Filename"] #: Dictionary containing name-:class:`Filename` pairs. _symbolicLinks: Dict[str, "SymbolicLink"] #: Dictionary containing name-:class:`SymbolicLink` pairs. _collapsed: bool #: True, if this directory was collapsed. It contains no subelements. _scanDuration: Nullable[float] #: Duration for scanning the directory and all its subelements. _aggregateDuration: Nullable[float] #: Duration for aggregating all subelements. def __init__( self, name: str, collectSubdirectories: bool = False, parent: Nullable["Directory"] = None ) -> None: """ Initialize the directory with name and parent reference. :param name: Name of the element. :param collectSubdirectories: If true, collect subdirectory statistics. :param parent: Optional parent reference. """ super().__init__(name, None, parent) self._path = None self._subdirectories = {} self._files = {} self._symbolicLinks = {} self._collapsed = False self._scanDuration = None self._aggregateDuration = None if parent is not None: parent._subdirectories[name] = self if parent._root is not None: self._root = parent._root if collectSubdirectories: self._collectSubdirectories() def _collectSubdirectories(self) -> None: """ Helper method for scanning subdirectories and aggregating found element sizes therein. """ with Stopwatch() as sw1: self._scanSubdirectories() with Stopwatch() as sw2: self._aggregateSizes() self._scanDuration = sw1.Duration self._aggregateDuration = sw2.Duration def _scanSubdirectories(self) -> None: """ Helper method for scanning subdirectories (recursively) and building a :class:`Directory`-:class:`Filename`-:class:`File` object tree. If a file refers to the same filesystem internal unique ID, a hardlink (two or more filenames) to the same file storage object is assumed. """ try: items = scandir(directoryPath := self.Path) except PermissionError as ex: return for dirEntry in items: if dirEntry.is_dir(follow_symlinks=False): subdirectory = Directory(dirEntry.name, collectSubdirectories=True, parent=self) elif dirEntry.is_file(follow_symlinks=False): id = dirEntry.inode() if id in self._root._ids: file = self._root._ids[id] hardLink = Filename(dirEntry.name, file=file, parent=self) else: s = dirEntry.stat(follow_symlinks=False) filename = Filename(dirEntry.name, parent=self) file = File(id, s.st_size, parent=filename) self._root._ids[id] = file elif dirEntry.is_symlink(): target = Path(readlink(directoryPath / dirEntry.name)) symlink = SymbolicLink(dirEntry.name, target, parent=self) else: raise FilesystemException(f"Unknown directory element.") def _connectSymbolicLinks(self) -> None: for dir in self._subdirectories.values(): dir._connectSymbolicLinks() for link in self._symbolicLinks.values(): if link._target.is_absolute(): pass else: target = self for elem in link._target.parts: if elem == ".": continue elif elem == "..": target = target._parent continue try: target = target._subdirectories[elem] continue except KeyError: pass try: target = target._files[elem] continue except KeyError: pass try: target = target._symbolicLinks[elem] continue except KeyError: pass target.AddLinkSources(link) def _aggregateSizes(self) -> None: self._size = ( sum(dir._size for dir in self._subdirectories.values()) + sum(file._file._size for file in self._files.values()) ) @Element.Root.setter def Root(self, value: "Root") -> None: Element.Root.fset(self, value) for subdir in self._subdirectories.values(): subdir.Root = value for file in self._files.values(): file.Root = value for link in self._symbolicLinks.values(): link.Root = value @Element.Parent.setter def Parent(self, value: _ParentType) -> None: Element.Parent.fset(self, value) value._subdirectories[self._name] = self if isinstance(value, Root): self.Root = value @readonly def Count(self) -> int: """ Read-only property to access the number of elements in a directory. :returns: Number of files plus subdirectories. """ return len(self._subdirectories) + len(self._files) + len(self._symbolicLinks) @readonly def FileCount(self) -> int: """ Read-only property to access the number of files in a directory. .. hint:: Files include regular files and symbolic links. :returns: Number of files. """ return len(self._files) + len(self._symbolicLinks) @readonly def RegularFileCount(self) -> int: """ Read-only property to access the number of regular files in a directory. :returns: Number of regular files. """ return len(self._files) @readonly def SymbolicLinkCount(self) -> int: """ Read-only property to access the number of symbolic links in a directory. :returns: Number of symbolic links. """ return len(self._symbolicLinks) @readonly def SubdirectoryCount(self) -> int: """ Read-only property to access the number of subdirectories in a directory. :returns: Number of subdirectories. """ return len(self._subdirectories) @readonly def TotalFileCount(self) -> int: """ Read-only property to access the total number of files in all child hierarchy levels (recursively). .. hint:: Files include regular files and symbolic links. :returns: Total number of files. """ return sum(d.TotalFileCount for d in self._subdirectories.values()) + len(self._files) + len(self._symbolicLinks) @readonly def TotalRegularFileCount(self) -> int: """ Read-only property to access the total number of regular files in all child hierarchy levels (recursively). :returns: Total number of regular files. """ return sum(d.TotalRegularFileCount for d in self._subdirectories.values()) + len(self._files) @readonly def TotalSymbolicLinkCount(self) -> int: """ Read-only property to access the total number of symbolic links in all child hierarchy levels (recursively). :returns: Total number of symbolic links. """ return sum(d.TotalSymbolicLinkCount for d in self._subdirectories.values()) + len(self._symbolicLinks) @readonly def TotalSubdirectoryCount(self) -> int: """ Read-only property to access the total number of subdirectories in all child hierarchy levels (recursively). :returns: Total number of subdirectories. """ return len(self._subdirectories) + sum(d.TotalSubdirectoryCount for d in self._subdirectories.values()) @readonly def Subdirectories(self) -> Generator["Directory", None, None]: """ Iterate all direct subdirectories of the directory. :returns: A generator to iterate all direct subdirectories. """ return (d for d in self._subdirectories.values()) @readonly def Files(self) -> Generator[Union["Filename", "SymbolicLink"], None, None]: """ Iterate all direct files of the directory. .. hint:: Files include regular files and symbolic links. :returns: A generator to iterate all direct files. """ return (f for f in chain(self._files.values(), self._symbolicLinks.values())) @readonly def RegularFiles(self) -> Generator["Filename", None, None]: """ Iterate all direct regular files of the directory. :returns: A generator to iterate all direct regular files. """ return (f for f in self._files.values()) @readonly def SymbolicLinks(self) -> Generator["SymbolicLink", None, None]: """ Iterate all direct symbolic links of the directory. :returns: A generator to iterate all direct symbolic links. """ return (l for l in self._symbolicLinks.values()) @readonly def Path(self) -> Path: """ Read-only property to access the equivalent Path instance for accessing the represented directory. :returns: Path to the directory. :raises FilesystemException: If no parent is set. """ if self._path is not None: return self._path if self._parent is None: raise FilesystemException(f"No parent or root set for directory.") self._path = self._parent.Path / self._name return self._path @readonly def ScanDuration(self) -> float: """ Read-only property to access the time needed to scan a directory structure including all subelements (recursively). :returns: The scan duration in seconds. :raises FilesystemException: If the directory was not scanned. """ if self._scanDuration is None: raise FilesystemException(f"Directory was not scanned, yet.") return self._scanDuration @readonly def AggregateDuration(self) -> float: """ Read-only property to access the time needed to aggregate the directory's and subelement's properties (recursively). :returns: The aggregation duration in seconds. :raises FilesystemException: If the directory properties were not aggregated. """ if self._scanDuration is None: raise FilesystemException(f"Directory properties were not aggregated, yet.") return self._aggregateDuration def Copy(self, parent: Nullable["Directory"] = None) -> "Directory": """ Copy the directory structure including all subelements and link it to the given parent. .. hint:: Statistics like aggregated directory size are copied too. |br| There is no rescan or repeated aggregation needed. :param parent: The parent element of the copied directory. :returns: A deep copy of the directory structure. """ dir = Directory(self._name, parent=parent) dir._size = self._size for subdir in self._subdirectories.values(): subdir.Copy(dir) for file in self._files.values(): file.Copy(dir) for link in self._symbolicLinks.values(): link.Copy(dir) return dir def Collapse(self, func: Callable[["Directory"], bool]) -> bool: # if len(self._subdirectories) == 0 or all(subdir.Collapse(func) for subdir in self._subdirectories.values()): if len(self._subdirectories) == 0: if func(self): # print(f"collapse 1 {self.Path}") self._collapsed = True self._subdirectories.clear() self._files.clear() self._symbolicLinks.clear() return True else: return False # if all(subdir.Collapse(func) for subdir in self._subdirectories.values()) collapsible = True for subdir in self._subdirectories.values(): result = subdir.Collapse(func) collapsible = collapsible and result if collapsible: # print(f"collapse 2 {self.Path}") self._collapsed = True self._subdirectories.clear() self._files.clear() self._symbolicLinks.clear() return True else: return False def ToTree(self, format: Nullable[Callable[[Node], str]] = None) -> Node: """ Convert the directory to a :class:`~pyTooling.Tree.Node`. The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the directory. Additional data is attached to the node's key-value store: ``kind`` The node's kind. See :class:`NodeKind`. ``size`` The directory's aggregated size. :param format: A user defined formatting function for tree nodes. :returns: A tree node representing this directory. """ if format is None: def format(node: Node) -> str: return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}" directoryNode = Node( value=self, keyValuePairs={ "kind": NodeKind.File, "size": self._size }, format=format ) directoryNode.AddChildren( e.ToTree(format) for e in chain(self._subdirectories.values()) #, self._files.values(), self._symbolicLinks.values()) ) return directoryNode def __eq__(self, other) -> bool: """ Compare two Directory instances for equality. :param other: Parameter to compare against. :returns: ``True``, if both directories and all its subelements are equal. :raises TypeError: If parameter ``other`` is not of type :class:`Directory`. """ if not isinstance(other, Directory): ex = TypeError("Parameter 'other' is not of type Directory.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex if not all(dir1 == dir2 for _, dir1, dir2 in zipdicts(self._subdirectories, other._subdirectories)): return False if not all(file1 == file2 for _, file1, file2 in zipdicts(self._files, other._files)): return False if not all(link1 == link2 for _, link1, link2 in zipdicts(self._symbolicLinks, other._symbolicLinks)): return False return True def __ne__(self, other: Any) -> bool: """ Compare two Directory instances for inequality. :param other: Parameter to compare against. :returns: ``True``, if both directories and all its subelements are unequal. :raises TypeError: If parameter ``other`` is not of type :class:`Directory`. """ return not self.__eq__(other) def __repr__(self) -> str: return f"Directory: {self.Path}" def __str__(self) -> str: return self._name @export class Filename(Element[Directory]): """ Represents a filename in the filesystem, but not the file storage object (:class:`File`). .. hint:: Filename and file storage are represented by two classes, which allows multiple names (hard links) per file storage object. """ _file: Nullable["File"] def __init__( self, name: str, file: Nullable["File"] = None, parent: Nullable[Directory] = None ) -> None: """ Initialize the filename with name, file (storage) object and parent reference. :param name: Name of the file. :param size: Optional file (storage) object. :param parent: Optional parent reference. """ super().__init__(name, None, parent) if file is None: self._file = None else: self._file = file file._parents.append(self) if parent is not None: parent._files[name] = self if parent._root is not None: self._root = parent._root @Element.Root.setter def Root(self, value: "Root") -> None: self._root = value if self._file is not None: self._file._root = value @Element.Parent.setter def Parent(self, value: _ParentType) -> None: Element.Parent.fset(self, value) value._files[self._name] = self if isinstance(value, Root): self.Root = value @readonly def File(self) -> Nullable["File"]: return self._file @readonly def Size(self) -> int: if self._file is None: raise ToolingException(f"Filename isn't linked to a File object.") return self._file._size @readonly def Path(self) -> Path: if self._parent is None: raise ToolingException(f"Filename has no parent object.") return self._parent.Path / self._name def Copy(self, parent: Directory) -> "Filename": fileID = self._file._id if fileID in parent._root._ids: file = parent._root._ids[fileID] else: fileSize = self._file._size file = File(fileID, fileSize) parent._root._ids[fileID] = file return Filename(self._name, file, parent=parent) def ToTree(self) -> Node: def format(node: Node) -> str: return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}" fileNode = Node( value=self, keyValuePairs={ "kind": NodeKind.File, "size": self._size }, format=format ) return fileNode def __eq__(self, other) -> bool: """ Compare two Filename instances for equality. :param other: Parameter to compare against. :returns: ``True``, if both filenames are equal. :raises TypeError: If parameter ``other`` is not of type :class:`Filename`. """ if not isinstance(other, Filename): ex = TypeError("Parameter 'other' is not of type Filename.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._name == other._name and self.Size == other.Size def __ne__(self, other: Any) -> bool: """ Compare two Filename instances for inequality. :param other: Parameter to compare against. :returns: ``True``, if both filenames are unequal. :raises TypeError: If parameter ``other`` is not of type :class:`Filename`. """ if not isinstance(other, Filename): ex = TypeError("Parameter 'other' is not of type Filename.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._name != other._name or self.Size != other.Size def __repr__(self) -> str: return f"File: {self.Path}" def __str__(self) -> str: return self._name @export class SymbolicLink(Element[Directory]): _target: Path def __init__( self, name: str, target: Path, parent: Nullable[Directory] ) -> None: super().__init__(name, None, parent) self._target = target if parent is not None: parent._symbolicLinks[name] = self if parent._root is not None: self._root = parent._root @readonly def Path(self) -> Path: return self._parent.Path / self._name @readonly def Target(self) -> Path: return self._target def Copy(self, parent: Directory) -> "SymbolicLink": return SymbolicLink(self._name, self._target, parent=parent) def ToTree(self) -> Node: def format(node: Node) -> str: return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}" symbolicLinkNode = Node( value=self, keyValuePairs={ "kind": NodeKind.SymbolicLink, "size": self._size }, format=format ) return symbolicLinkNode def __eq__(self, other) -> bool: """ Compare two SymbolicLink instances for equality. :param other: Parameter to compare against. :returns: ``True``, if both symbolic links are equal. :raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`. """ if not isinstance(other, SymbolicLink): ex = TypeError("Parameter 'other' is not of type SymbolicLink.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._name == other._name and self._target == other._target def __ne__(self, other: Any) -> bool: """ Compare two SymbolicLink instances for inequality. :param other: Parameter to compare against. :returns: ``True``, if both symbolic links are unequal. :raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`. """ if not isinstance(other, SymbolicLink): ex = TypeError("Parameter 'other' is not of type SymbolicLink.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._name != other._name or self._target != other._target def __repr__(self) -> str: return f"SymLink: {self.Path} -> {self._target}" def __str__(self) -> str: return self._name @export class Root(Directory): """ A **Root** represents the root-directory in the filesystem, which contains subdirectories, regular files and symbolic links. """ _ids: Dict[int, "File"] #: Dictionary of file identifier - file objects pairs found while scanning the directory structure. def __init__( self, rootDirectory: Path, collectSubdirectories: bool = True ) -> None: if rootDirectory is None: raise ValueError(f"Parameter 'rootDirectory' is None.") elif not isinstance(rootDirectory, Path): raise TypeError(f"Parameter 'rootDirectory' is not of type 'Path'.") elif not rootDirectory.exists(): raise ToolingException(f"Path '{rootDirectory}' doesn't exist.") from FileNotFoundError(rootDirectory) self._ids = {} super().__init__(rootDirectory.name) self._root = self self._path = rootDirectory if collectSubdirectories: self._collectSubdirectories() self._connectSymbolicLinks() @readonly def TotalHardLinkCount(self) -> int: return sum(l for f in self._ids.values() if (l := len(f._parents)) > 1) @readonly def TotalHardLinkCount2(self) -> int: return sum(1 for f in self._ids.values() if len(f._parents) > 1) @readonly def TotalHardLinkCount3(self) -> int: return sum(1 for f in self._ids.values() if len(f._parents) == 1) @readonly def Size2(self) -> int: return sum(f._size for f in self._ids.values() if len(f._parents) > 1) @readonly def Size3(self) -> int: return sum(f._size * len(f._parents) for f in self._ids.values() if len(f._parents) > 1) @readonly def TotalUniqueFileCount(self) -> int: return len(self._ids) @readonly def Path(self) -> Path: """ Read-only property to access the path of the filesystem statistics root. :returns: Path to the root of the filesystem statistics root directory. """ return self._path def Copy(self) -> "Root": """ Copy the directory structure including all subelements and link it to the given parent. The duration for the deep copy process is provided in :attr:`ScanDuration` .. hint:: Statistics like aggregated directory size are copied too. |br| There is no rescan or repeated aggregation needed. :returns: A deep copy of the directory structure. """ with Stopwatch() as sw: root = Root(self._path, False) root._size = self._size for subdir in self._subdirectories.values(): subdir.Copy(root) for file in self._files.values(): file.Copy(root) for link in self._symbolicLinks.values(): link.Copy(root) root._scanDuration = sw.Duration root._aggregateDuration = 0.0 return root def __repr__(self) -> str: return f"Root: {self.Path} (dirs: {self.TotalSubdirectoryCount}, files: {self.TotalRegularFileCount}, symlinks: {self.TotalSymbolicLinkCount})" def __str__(self) -> str: return self._name @export class File(Base): """ A **File** represents a file storage object in the filesystem, which is accessible by one or more :class:`Filename` objects. Each file has an internal id, which is associated to a unique ID within the host's filesystem. """ _id: int #: Unique (host internal) file object ID) _parents: List[Filename] #: List of reverse references to :class:`Filename` objects. def __init__( self, id: int, size: int, parent: Nullable[Filename] = None ) -> None: """ Initialize the File storage object with an ID, size and parent reference. :param id: Unique ID of the file object. :param size: Size of the file object. :param parent: Optional parent reference. """ if not isinstance(id, int): ex = TypeError("Parameter 'id' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(id)}'.") raise ex self._id = id if parent is None: super().__init__(size, None) self._parents = [] elif isinstance(parent, Filename): super().__init__(size, parent._root) self._parents = [parent] parent._file = self else: ex = TypeError("Parameter 'parent' is not of type 'Filename'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex @readonly def ID(self) -> int: """ Read-only property to access the file object's unique identifier. :returns: Unique file object identifier. """ return self._id @readonly def Parents(self) -> List[Filename]: """ Read-only property to access the list of filenames using the same file storage object. .. hint:: This allows to check if a file object has multiple filenames a.k.a hardlinks. :returns: List of filenames for the file storage object. """ return self._parents def AddParent(self, file: Filename) -> None: """ Add another parent reference to a :class:`Filename`. :param file: Reference to a filename object. """ if not isinstance(file, Filename): ex = TypeError("Parameter 'file' is not of type 'Filename'.") ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") raise ex elif file._file is not None: raise ToolingException(f"Filename is already referencing an other file object ({file._file._id}).") self._parents.append(file) file._file = self if file._root is not None: self._root = file._root pyTooling-8.11.0/pyTooling/GenericPath/000077500000000000000000000000001513317154500177475ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/GenericPath/URL.py000066400000000000000000000333721513317154500207730ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| ___ _ __ ___ _ __(_) ___| _ \ __ _| |_| |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _ / _ \ '_ \ / _ \ '__| |/ __| |_) / _` | __| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | __/ | | | __/ | | | (__| __/ (_| | |_| | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|\___|_| |_|\___|_| |_|\___|_| \__,_|\__|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ This package provides a representation for a Uniform Resource Locator (URL). .. code-block:: [schema://][user[:password]@]domain.tld[:port]/path/to/file[?query][#fragment] """ from enum import IntFlag from re import compile as re_compile from typing import Dict, Optional as Nullable, Mapping try: from pyTooling.Decorators import export, readonly from pyTooling.Exceptions import ToolingException from pyTooling.Common import getFullyQualifiedName from pyTooling.GenericPath import RootMixIn, ElementMixIn, PathMixIn except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.GenericPath.URL] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from Exceptions import ToolingException from Common import getFullyQualifiedName from GenericPath import RootMixIn, ElementMixIn, PathMixIn except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.GenericPath.URL] Could not import directly!") raise ex __all__ = ["URL_PATTERN", "URL_REGEXP"] URL_PATTERN = ( r"""(?:(?P\w+)://)?""" r"""(?:(?P[-a-zA-Z0-9_]+)(?::(?P[-a-zA-Z0-9_]+))?@)?""" r"""(?:(?P(?:[-a-zA-Z0-9_]+)(?:\.[-a-zA-Z0-9_]+)*\.?)(?:\:(?P\d+))?)?""" r"""(?P[^?#]*?)""" r"""(?:\?(?P[^#]+?))?""" r"""(?:#(?P.+?))?""" ) #: Regular expression pattern for validating and splitting a URL. URL_REGEXP = re_compile("^" + URL_PATTERN + "$") #: Precompiled regular expression for URL validation. @export class Protocols(IntFlag): """Enumeration of supported URL schemes.""" TLS = 1 #: Transport Layer Security HTTP = 2 #: Hyper Text Transfer Protocol HTTPS = 4 #: SSL/TLS secured HTTP FTP = 8 #: File Transfer Protocol FTPS = 16 #: SSL/TLS secured FTP FILE = 32 #: Local files @export class Host(RootMixIn): """Represents a host as either hostname, DNS or IP-address including the port number in a URL.""" _hostname: str #: Name of the host (DNS name or IP address). _port: Nullable[int] #: Optional port number. def __init__( self, hostname: str, port: Nullable[int] = None ) -> None: """ Initialize a host instance described by host name and port number. :param hostname: Name of the host (either IP address or DNS). :param port: Port number. """ super().__init__() if not isinstance(hostname, str): ex = TypeError("Parameter 'hostname' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(hostname)}'.") raise ex self._hostname = hostname if port is None: pass elif not isinstance(port, int): ex = TypeError("Parameter 'port' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(port)}'.") raise ex elif not (0 <= port < 65536): ex = ValueError("Parameter 'port' is out of range 0..65535.") ex.add_note(f"Got value '{port}'.") raise ex self._port = port @readonly def Hostname(self) -> str: """ Read-only property to access the hostname. :returns: Hostname as DNS name or IP address. """ return self._hostname @readonly def Port(self) -> Nullable[int]: """ Read-only property to access the optional port number. :returns: Optional port number. """ return self._port def __str__(self) -> str: result = self._hostname if self._port is not None: result += f":{self._port}" return result def Copy(self) -> "Host": """ Create a copy of this object. :return: A new :class:`Host` instance. """ return self.__class__( self._hostname, self._port ) @export class Element(ElementMixIn): """Derived class for the URL context.""" @export class Path(PathMixIn): """Represents a path in a URL.""" ELEMENT_DELIMITER = "/" #: Delimiter symbol in URLs between path elements. ROOT_DELIMITER = "/" #: Delimiter symbol in URLs between root element and first path element. @classmethod def Parse(cls, path: str, root: Nullable[Host] = None) -> "Path": return super().Parse(path, root, cls, Element) @export class URL: """ Represents a URL (Uniform Resource Locator) including scheme, host, credentials, path, query and fragment. .. code-block:: [schema://][user[:password]@]domain.tld[:port]/path/to/file[?query][#fragment] """ _scheme: Protocols _user: Nullable[str] _password: Nullable[str] _host: Nullable[Host] _path: Path _query: Nullable[Dict[str, str]] _fragment: Nullable[str] def __init__( self, scheme: Protocols, path: Path, host: Nullable[Host] = None, user: Nullable[str] = None, password: Nullable[str] = None, query: Nullable[Mapping[str, str]] = None, fragment: Nullable[str] = None ) -> None: """ Initializes a Uniform Resource Locator (URL). :param scheme: Transport scheme to be used for a specified resource. :param path: Path to the resource. :param host: Hostname where the resource is located. :param user: Username for basic authentication. :param password: Password for basic authentication. :param query: An optional query string. :param fragment: An optional fragment. """ if scheme is not None and not isinstance(scheme, Protocols): ex = TypeError("Parameter 'scheme' is not of type 'Protocols'.") ex.add_note(f"Got type '{getFullyQualifiedName(scheme)}'.") raise ex self._scheme = scheme if user is not None and not isinstance(user, str): ex = TypeError("Parameter 'user' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(user)}'.") raise ex self._user = user if password is not None and not isinstance(password, str): ex = TypeError(f"Parameter 'password' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(password)}'.") raise ex self._password = password if host is not None and not isinstance(host, Host): ex = TypeError(f"Parameter 'host' is not of type 'Host'.") ex.add_note(f"Got type '{getFullyQualifiedName(host)}'.") raise ex self._host = host if path is not None and not isinstance(path, Path): ex = TypeError(f"Parameter 'path' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") raise ex self._path = path if query is not None: if not isinstance(query, Mapping): ex = TypeError(f"Parameter 'query' is not a mapping ('dict', ...).") ex.add_note(f"Got type '{getFullyQualifiedName(query)}'.") raise ex self._query = {keyword: value for keyword, value in query.items()} else: self._query = None if fragment is not None and not isinstance(fragment, str): ex = TypeError(f"Parameter 'fragment' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(fragment)}'.") raise ex self._fragment = fragment @readonly def Scheme(self) -> Protocols: """ Read-only property to access the URL scheme. :returns: URL scheme of the URL. """ return self._scheme @readonly def User(self) -> Nullable[str]: """ Read-only property to access the optional username. :returns: Optional username within the URL. """ return self._user @readonly def Password(self) -> Nullable[str]: """ Read-only property to access the optional password. :returns: Optional password within a URL. """ return self._password @readonly def Host(self) -> Nullable[Host]: """ Read-only property to access the host part (hostname and port number) of the URL. :returns: The host part of the URL. """ return self._host @readonly def Path(self) -> Path: """ Read-only property to access the path part of the URL. :returns: Path part of the URL. """ return self._path @readonly def Query(self) -> Nullable[Dict[str, str]]: """ Read-only property to access the dictionary of key-value pairs representing the query part in the URL. :returns: A dictionary representing the query as key-value pairs. """ return self._query @readonly def Fragment(self) -> Nullable[str]: """ Read-only property to access the fragment part of the URL. :returns: The fragment part of the URL. """ return self._fragment # http://semaphore.plc2.de:5000/api/v1/semaphore?name=Riviera&foo=bar#page2 @classmethod def Parse(cls, url: str) -> "URL": """ Parse a URL string and returns the URL object. :param url: URL as string to be parsed. :returns: A URL object. :raises ToolingException: When syntax does not match. """ if (matches := URL_REGEXP.match(url)) is not None: scheme = matches.group("scheme") user = matches.group("user") password = matches.group("password") host = matches.group("host") port = matches.group("port") if port is not None: port = int(port) path = matches.group("path") query = matches.group("query") fragment = matches.group("fragment") scheme = None if scheme is None else Protocols[scheme.upper()] hostObj = None if host is None else Host(host, port) pathObj = Path.Parse(path, hostObj) parameters = {} if query is not None: for pair in query.split("&"): key, value = pair.split("=") parameters[key] = value return cls( scheme, pathObj, hostObj, user, password, parameters if len(parameters) > 0 else None, fragment ) raise ToolingException(f"Syntax error when parsing URL '{url}'.") def __str__(self) -> str: """ Formats the URL object as a string representation. :returns: Formatted URL object. """ result = str(self._path) if self._host is not None: result = str(self._host) + result if self._user is not None: if self._password is not None: result = f"{self._user}:{self._password}@{result}" else: result = f"{self._user}@{result}" if self._scheme is not None: result = self._scheme.name.lower() + "://" + result if self._query is not None and len(self._query) > 0: result = result + "?" + "&".join([f"{key}={value}" for key, value in self._query.items()]) if self._fragment is not None: result = result + "#" + self._fragment return result def WithoutCredentials(self) -> "URL": """ Returns a URL object without credentials (username and password). :returns: New URL object without credentials. """ return self.__class__( scheme=self._scheme, path=self._path, host=self._host, query=self._query, fragment=self._fragment ) pyTooling-8.11.0/pyTooling/GenericPath/__init__.py000066400000000000000000000161021513317154500220600ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| ___ _ __ ___ _ __(_) ___| _ \ __ _| |_| |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _ / _ \ '_ \ / _ \ '__| |/ __| |_) / _` | __| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | __/ | | | __/ | | | (__| __/ (_| | |_| | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|\___|_| |_|\___|_| |_|\___|_| \__,_|\__|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """A generic path to derive domain specific path libraries.""" from typing import List, Optional as Nullable, Type try: from pyTooling.Decorators import export from pyTooling.MetaClasses import ExtendedType except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.GenericPath] Could not import from 'pyTooling.*'!") try: from Decorators import export from MetaClasses import ExtendedType except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.GenericPath] Could not import directly!") raise ex @export class Base(metaclass=ExtendedType, mixin=True): """Base-mixin-class for all :mod:`pyTooling.GenericPath` path elements.""" DELIMITER = "/" #: Path element delimiter sign. _parent: Nullable["Base"] #: Reference to the parent object. def __init__(self, parent: Nullable["Base"] = None) -> None: """ Initialize the base-mixin-class with a parent reference. :param parent: Optional parent reference. """ self._parent = parent @export class RootMixIn(Base, mixin=True): """Mixin-class for root elements in a path system.""" def __init__(self) -> None: """ Initialize the mixin-class for a root element. """ super().__init__(None) @export class ElementMixIn(Base, mixin=True): """Mixin-class for elements in a path system.""" _elementName: str #: Name of the path element. def __init__(self, parent: Base, elementName: str) -> None: """ Initialize the mixin-class for a path element. :param parent: Reference to a parent path element. :param elementName: Name of the path element. """ super().__init__(parent) self._elementName = elementName def __str__(self) -> str: return self._elementName @export class PathMixIn(metaclass=ExtendedType, mixin=True): """Mixin-class for a path.""" ELEMENT_DELIMITER = "/" #: Path element delimiter sign. ROOT_DELIMITER = "/" #: Root element delimiter sign. _isAbsolute: bool #: True, if the path is absolute. _elements: List[ElementMixIn] #: List of path elements. def __init__(self, elements: List[ElementMixIn], isAbsolute: bool) -> None: """ Initialize the mixin-class for a path. :param elements: Reference to a parent path element. :param isAbsolute: Assign to true, if a path is absolute, otherwise false. """ self._isAbsolute = isAbsolute self._elements = elements def __len__(self) -> int: """ Returns the number of path elements. :returns: Number of path elements. """ return len(self._elements) def __str__(self) -> str: result = self.ROOT_DELIMITER if self._isAbsolute else "" if len(self._elements) > 0: result = result + str(self._elements[0]) for element in self._elements[1:]: result = result + self.ELEMENT_DELIMITER + str(element) return result @classmethod def Parse( cls, path: str, root: RootMixIn, pathCls: Type["PathMixIn"], elementCls: Type[ElementMixIn] ) -> "PathMixIn": """ Parses a string representation of a path and returns a path instance. :param path: Path to be parsed. :param root: :param pathCls: Type used to create the path. :param elementCls: Type used to create the path elements. :return: """ if path.startswith(cls.ROOT_DELIMITER): isAbsolute = True path = path[len(cls.ELEMENT_DELIMITER):] else: isAbsolute = False parent = root elements = [] for part in path.split(cls.ELEMENT_DELIMITER): element = elementCls(parent, part) parent = element elements.append(element) return pathCls(elements, isAbsolute) @export class SystemMixIn(metaclass=ExtendedType, mixin=True): """Mixin-class for a path system.""" pyTooling-8.11.0/pyTooling/Graph/000077500000000000000000000000001513317154500166175ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Graph/GraphML.py000066400000000000000000000502541513317154500204710ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ A data model to write out GraphML XML files. .. seealso:: * http://graphml.graphdrawing.org/primer/graphml-primer.html """ from enum import Enum, auto from pathlib import Path from typing import Any, List, Dict, Union, Optional as Nullable try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyTooling.Graph import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph from pyTooling.Tree import Node as pyToolingNode except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Graph.GraphML] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, mixin from Graph import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph from Tree import Node as pyToolingNode except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Graph.GraphML] Could not import directly!") raise ex @export class AttributeContext(Enum): """ Enumeration of all attribute contexts. An attribute context describes to what kind of GraphML node an attribute can be applied. """ GraphML = auto() Graph = auto() Node = auto() Edge = auto() Port = auto() def __str__(self) -> str: return f"{self.name.lower()}" @export class AttributeTypes(Enum): """ Enumeration of all attribute types. An attribute type describes what datatype can be applied to an attribute. """ Boolean = auto() Int = auto() Long = auto() Float = auto() Double = auto() String = auto() def __str__(self) -> str: return f"{self.name.lower()}" @export class EdgeDefault(Enum): """An enumeration describing the default edge direction.""" Undirected = auto() Directed = auto() def __str__(self) -> str: return f"{self.name.lower()}" @export class ParsingOrder(Enum): """An enumeration describing the parsing order of the graph's representation.""" NodesFirst = auto() #: First, all nodes are given, then followed by all edges. AdjacencyList = auto() Free = auto() def __str__(self) -> str: return f"{self.name.lower()}" @export class IDStyle(Enum): """An enumeration describing the style of identifiers (IDs).""" Canonical = auto() Free = auto() def __str__(self) -> str: return f"{self.name.lower()}" @export class Base(metaclass=ExtendedType, slots=True): """ Base-class for all GraphML data model classes. """ @readonly def HasClosingTag(self) -> bool: return True def Tag(self, indent: int = 0) -> str: raise NotImplementedError() def OpeningTag(self, indent: int = 0) -> str: raise NotImplementedError() def ClosingTag(self, indent: int = 0) -> str: raise NotImplementedError() def ToStringLines(self, indent: int = 0) -> List[str]: raise NotImplementedError() @export class BaseWithID(Base): _id: str def __init__(self, identifier: str) -> None: super().__init__() self._id = identifier @readonly def ID(self) -> str: return self._id @export class BaseWithData(BaseWithID): _data: List['Data'] def __init__(self, identifier: str) -> None: super().__init__(identifier) self._data = [] @readonly def Data(self) -> List['Data']: return self._data def AddData(self, data: Data) -> Data: self._data.append(data) return data @export class Key(BaseWithID): _context: AttributeContext _attributeName: str _attributeType: AttributeTypes def __init__(self, identifier: str, context: AttributeContext, name: str, type: AttributeTypes) -> None: super().__init__(identifier) self._context = context self._attributeName = name self._attributeType = type @readonly def Context(self) -> AttributeContext: return self._context @readonly def AttributeName(self) -> str: return self._attributeName @readonly def AttributeType(self) -> AttributeTypes: return self._attributeType @readonly def HasClosingTag(self) -> bool: return False def Tag(self, indent: int = 2) -> str: return f"""{' '*indent}\n""" def ToStringLines(self, indent: int = 2) -> List[str]: return [self.Tag(indent)] @export class Data(Base): _key: Key _data: Any def __init__(self, key: Key, data: Any) -> None: super().__init__() self._key = key self._data = data @readonly def Key(self) -> Key: return self._key @readonly def Data(self) -> Any: return self._data @readonly def HasClosingTag(self) -> bool: return False def Tag(self, indent: int = 2) -> str: data = str(self._data) data = data.replace("&", "&") data = data.replace("<", "<") data = data.replace(">", ">") data = data.replace("\n", "\\n") return f"""{' '*indent}{data}\n""" def ToStringLines(self, indent: int = 2) -> List[str]: return [self.Tag(indent)] @export class Node(BaseWithData): def __init__(self, identifier: str) -> None: super().__init__(identifier) @readonly def HasClosingTag(self) -> bool: return len(self._data) > 0 def Tag(self, indent: int = 2) -> str: return f"""{' '*indent}\n""" def OpeningTag(self, indent: int = 2) -> str: return f"""{' '*indent}\n""" def ClosingTag(self, indent: int = 2) -> str: return f"""{' ' * indent}\n""" def ToStringLines(self, indent: int = 2) -> List[str]: if not self.HasClosingTag: return [self.Tag(indent)] lines = [self.OpeningTag(indent)] for data in self._data: lines.extend(data.ToStringLines(indent + 1)) lines.append(self.ClosingTag(indent)) return lines @export class Edge(BaseWithData): _source: Node _target: Node def __init__(self, identifier: str, source: Node, target: Node) -> None: super().__init__(identifier) self._source = source self._target = target @readonly def Source(self) -> Node: return self._source @readonly def Target(self) -> Node: return self._target @readonly def HasClosingTag(self) -> bool: return len(self._data) > 0 def Tag(self, indent: int = 2) -> str: return f"""{' ' * indent}\n""" def OpeningTag(self, indent: int = 2) -> str: return f"""{' '*indent}\n""" def ClosingTag(self, indent: int = 2) -> str: return f"""{' ' * indent}\n""" def ToStringLines(self, indent: int = 2) -> List[str]: if not self.HasClosingTag: return [self.Tag(indent)] lines = [self.OpeningTag(indent)] for data in self._data: lines.extend(data.ToStringLines(indent + 1)) lines.append(self.ClosingTag(indent)) return lines @export class BaseGraph(BaseWithData, mixin=True): _subgraphs: Dict[str, 'Subgraph'] _nodes: Dict[str, Node] _edges: Dict[str, Edge] _edgeDefault: EdgeDefault _parseOrder: ParsingOrder _nodeIDStyle: IDStyle _edgeIDStyle: IDStyle def __init__(self, identifier: Nullable[str] = None) -> None: super().__init__(identifier) self._subgraphs = {} self._nodes = {} self._edges = {} self._edgeDefault = EdgeDefault.Directed self._parseOrder = ParsingOrder.NodesFirst self._nodeIDStyle = IDStyle.Free self._edgeIDStyle = IDStyle.Free @readonly def Subgraphs(self) -> Dict[str, 'Subgraph']: return self._subgraphs @readonly def Nodes(self) -> Dict[str, Node]: return self._nodes @readonly def Edges(self) -> Dict[str, Edge]: return self._edges def AddSubgraph(self, subgraph: 'Subgraph') -> 'Subgraph': self._subgraphs[subgraph._subgraphID] = subgraph self._nodes[subgraph._id] = subgraph return subgraph def GetSubgraph(self, subgraphName: str) -> 'Subgraph': return self._subgraphs[subgraphName] def AddNode(self, node: Node) -> Node: self._nodes[node._id] = node return node def GetNode(self, nodeName: str) -> Node: return self._nodes[nodeName] def AddEdge(self, edge: Edge) -> Edge: self._edges[edge._id] = edge return edge def GetEdge(self, edgeName: str) -> Edge: return self._edges[edgeName] def OpeningTag(self, indent: int = 1) -> str: return f"""\ {' '*indent} """ def ClosingTag(self, indent: int = 1) -> str: return f"{' '*indent}\n" def ToStringLines(self, indent: int = 1) -> List[str]: lines = [self.OpeningTag(indent)] for node in self._nodes.values(): lines.extend(node.ToStringLines(indent + 1)) for edge in self._edges.values(): lines.extend(edge.ToStringLines(indent + 1)) # for data in self._data: # lines.extend(data.ToStringLines(indent + 1)) lines.append(self.ClosingTag(indent)) return lines @export class Graph(BaseGraph): _document: 'GraphMLDocument' _ids: Dict[str, Union[Node, Edge, 'Subgraph']] def __init__(self, document: 'GraphMLDocument', identifier: str) -> None: super().__init__(identifier) self._document = document self._ids = {} def GetByID(self, identifier: str) -> Union[Node, Edge, 'Subgraph']: return self._ids[identifier] def AddSubgraph(self, subgraph: 'Subgraph') -> 'Subgraph': result = super().AddSubgraph(subgraph) self._ids[subgraph._subgraphID] = subgraph subgraph._root = self return result def AddNode(self, node: Node) -> Node: result = super().AddNode(node) self._ids[node._id] = node return result def AddEdge(self, edge: Edge) -> Edge: result = super().AddEdge(edge) self._ids[edge._id] = edge return result @export class Subgraph(Node, BaseGraph): _subgraphID: str _root: Nullable[Graph] def __init__(self, nodeIdentifier: str, graphIdentifier: str) -> None: super().__init__(nodeIdentifier) BaseGraph.__init__(self, nodeIdentifier) self._subgraphID = graphIdentifier self._root = None @readonly def RootGraph(self) -> Graph: return self._root @readonly def SubgraphID(self) -> str: return self._subgraphID @readonly def HasClosingTag(self) -> bool: return True def AddNode(self, node: Node) -> Node: result = super().AddNode(node) self._root._ids[node._id] = node return result def AddEdge(self, edge: Edge) -> Edge: result = super().AddEdge(edge) self._root._ids[edge._id] = edge return result def Tag(self, indent: int = 2) -> str: raise NotImplementedError() def OpeningTag(self, indent: int = 1) -> str: return f"""\ {' ' * indent} """ def ClosingTag(self, indent: int = 2) -> str: return BaseGraph.ClosingTag(self, indent) def ToStringLines(self, indent: int = 2) -> List[str]: lines = [super().OpeningTag(indent)] for data in self._data: lines.extend(data.ToStringLines(indent + 1)) # lines.extend(Graph.ToStringLines(self, indent + 1)) lines.append(self.OpeningTag(indent + 1)) for node in self._nodes.values(): lines.extend(node.ToStringLines(indent + 2)) for edge in self._edges.values(): lines.extend(edge.ToStringLines(indent + 2)) # for data in self._data: # lines.extend(data.ToStringLines(indent + 1)) lines.append(self.ClosingTag(indent + 1)) lines.append(super().ClosingTag(indent)) return lines @export class GraphMLDocument(Base): xmlNS = { None: "http://graphml.graphdrawing.org/xmlns", "xsi": "http://www.w3.org/2001/XMLSchema-instance" } xsi = { "schemaLocation": "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd" } _graph: Graph _keys: Dict[str, Key] def __init__(self, identifier: str = "G") -> None: super().__init__() self._graph = Graph(self, identifier) self._keys = {} @readonly def Graph(self) -> BaseGraph: return self._graph @readonly def Keys(self) -> Dict[str, Key]: return self._keys def AddKey(self, key: Key) -> Key: self._keys[key._id] = key return key def GetKey(self, keyName: str) -> Key: return self._keys[keyName] def HasKey(self, keyName: str) -> bool: return keyName in self._keys def FromGraph(self, graph: pyToolingGraph) -> None: document = self self._graph._id = graph._name nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String)) edgeValue = self.AddKey(Key("edgeValue", AttributeContext.Edge, "value", AttributeTypes.String)) def translateGraph(rootGraph: Graph, pyTGraph: pyToolingGraph): for vertex in pyTGraph.IterateVertices(): newNode = Node(vertex._id) newNode.AddData(Data(nodeValue, vertex._value)) for key, value in vertex._dict.items(): if document.HasKey(str(key)): nodeKey = document.GetKey(f"node{key!s}") else: nodeKey = document.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String)) newNode.AddData(Data(nodeKey, value)) rootGraph.AddNode(newNode) for edge in pyTGraph.IterateEdges(): source = rootGraph.GetByID(edge._source._id) target = rootGraph.GetByID(edge._destination._id) newEdge = Edge(edge._id, source, target) newEdge.AddData(Data(edgeValue, edge._value)) for key, value in edge._dict.items(): if self.HasKey(str(key)): edgeKey = self.GetBy(f"edge{key!s}") else: edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String)) newEdge.AddData(Data(edgeKey, value)) rootGraph.AddEdge(newEdge) for link in pyTGraph.IterateLinks(): source = rootGraph.GetByID(link._source._id) target = rootGraph.GetByID(link._destination._id) newEdge = Edge(link._id, source, target) newEdge.AddData(Data(edgeValue, link._value)) for key, value in link._dict.items(): if self.HasKey(str(key)): edgeKey = self.GetKey(f"link{key!s}") else: edgeKey = self.AddKey(Key(f"link{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String)) newEdge.AddData(Data(edgeKey, value)) rootGraph.AddEdge(newEdge) def translateSubgraph(nodeGraph: Subgraph, pyTSubgraph: pyToolingSubgraph): rootGraph = nodeGraph.RootGraph for vertex in pyTSubgraph.IterateVertices(): newNode = Node(vertex._id) newNode.AddData(Data(nodeValue, vertex._value)) for key, value in vertex._dict.items(): if self.HasKey(str(key)): nodeKey = self.GetKey(f"node{key!s}") else: nodeKey = self.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String)) newNode.AddData(Data(nodeKey, value)) nodeGraph.AddNode(newNode) for edge in pyTSubgraph.IterateEdges(): source = nodeGraph.GetNode(edge._source._id) target = nodeGraph.GetNode(edge._destination._id) newEdge = Edge(edge._id, source, target) newEdge.AddData(Data(edgeValue, edge._value)) for key, value in edge._dict.items(): if self.HasKey(str(key)): edgeKey = self.GetKey(f"edge{key!s}") else: edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String)) newEdge.AddData(Data(edgeKey, value)) nodeGraph.AddEdge(newEdge) for subgraph in graph.Subgraphs: nodeGraph = Subgraph(subgraph.Name, "sg" + subgraph.Name) self._graph.AddSubgraph(nodeGraph) translateSubgraph(nodeGraph, subgraph) translateGraph(self._graph, graph) def FromTree(self, tree: pyToolingNode) -> None: self._graph._id = tree._id nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String)) rootNode = self._graph.AddNode(Node(tree._id)) rootNode.AddData(Data(nodeValue, tree._value)) for i, node in enumerate(tree.GetDescendants()): newNode = self._graph.AddNode(Node(node._id)) newNode.AddData(Data(nodeValue, node._value)) newEdge = self._graph.AddEdge(Edge(f"e{i}", newNode, self._graph.GetNode(node._parent._id))) def OpeningTag(self, indent: int = 0) -> str: return f"""\ {' '*indent} """ def ClosingTag(self, indent: int = 0) -> str: return f"{' '*indent}\n" def ToStringLines(self, indent: int = 0) -> List[str]: lines = [self.OpeningTag(indent)] for key in self._keys.values(): lines.extend(key.ToStringLines(indent + 1)) lines.extend(self._graph.ToStringLines(indent + 1)) lines.append(self.ClosingTag(indent)) return lines def WriteToFile(self, file: Path) -> None: with file.open("w", encoding="utf-8") as f: f.write(f"""""") f.writelines(self.ToStringLines()) pyTooling-8.11.0/pyTooling/Graph/__init__.py000066400000000000000000003155301513317154500207370ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ A powerful **graph** data structure for Python. Graph algorithms using all vertices are provided as methods on the graph instance. Whereas graph algorithms based on a starting vertex are provided as methods on a vertex. .. admonition:: Example Graph .. mermaid:: :caption: A directed graph with backward-edges denoted by dotted vertex relations. %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph LR A(A); B(B); C(C); D(D); E(E); F(F) ; G(G); H(H); I(I) A --> B --> E G --> F A --> C --> G --> H --> D D -.-> A D & F -.-> B I ---> E --> F --> D classDef node fill:#eee,stroke:#777,font-size:smaller; """ import heapq from collections import deque from itertools import chain from typing import TypeVar, Generic, List, Tuple, Dict, Set, Deque, Union, Optional as Nullable from typing import Callable, Iterator as typing_Iterator, Generator, Iterable, Mapping, Hashable try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyTooling.Exceptions import ToolingException from pyTooling.Common import getFullyQualifiedName from pyTooling.Tree import Node except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Graph] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, mixin from Exceptions import ToolingException from Common import getFullyQualifiedName from Tree import Node except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Graph] Could not import directly!") raise ex DictKeyType = TypeVar("DictKeyType", bound=Hashable) """A type variable for dictionary keys.""" DictValueType = TypeVar("DictValueType") """A type variable for dictionary values.""" IDType = TypeVar("IDType", bound=Hashable) """A type variable for an ID.""" WeightType = TypeVar("WeightType", bound=Union[int, float]) """A type variable for a weight.""" ValueType = TypeVar("ValueType") """A type variable for a value.""" VertexIDType = TypeVar("VertexIDType", bound=Hashable) """A type variable for a vertex's ID.""" VertexWeightType = TypeVar("VertexWeightType", bound=Union[int, float]) """A type variable for a vertex's weight.""" VertexValueType = TypeVar("VertexValueType") """A type variable for a vertex's value.""" VertexDictKeyType = TypeVar("VertexDictKeyType", bound=Hashable) """A type variable for a vertex's dictionary keys.""" VertexDictValueType = TypeVar("VertexDictValueType") """A type variable for a vertex's dictionary values.""" EdgeIDType = TypeVar("EdgeIDType", bound=Hashable) """A type variable for an edge's ID.""" EdgeWeightType = TypeVar("EdgeWeightType", bound=Union[int, float]) """A type variable for an edge's weight.""" EdgeValueType = TypeVar("EdgeValueType") """A type variable for an edge's value.""" EdgeDictKeyType = TypeVar("EdgeDictKeyType", bound=Hashable) """A type variable for an edge's dictionary keys.""" EdgeDictValueType = TypeVar("EdgeDictValueType") """A type variable for an edge's dictionary values.""" LinkIDType = TypeVar("LinkIDType", bound=Hashable) """A type variable for an link's ID.""" LinkWeightType = TypeVar("LinkWeightType", bound=Union[int, float]) """A type variable for an link's weight.""" LinkValueType = TypeVar("LinkValueType") """A type variable for an link's value.""" LinkDictKeyType = TypeVar("LinkDictKeyType", bound=Hashable) """A type variable for an link's dictionary keys.""" LinkDictValueType = TypeVar("LinkDictValueType") """A type variable for an link's dictionary values.""" ComponentDictKeyType = TypeVar("ComponentDictKeyType", bound=Hashable) """A type variable for a component's dictionary keys.""" ComponentDictValueType = TypeVar("ComponentDictValueType") """A type variable for a component's dictionary values.""" SubgraphDictKeyType = TypeVar("SubgraphDictKeyType", bound=Hashable) """A type variable for a component's dictionary keys.""" SubgraphDictValueType = TypeVar("SubgraphDictValueType") """A type variable for a component's dictionary values.""" ViewDictKeyType = TypeVar("ViewDictKeyType", bound=Hashable) """A type variable for a component's dictionary keys.""" ViewDictValueType = TypeVar("ViewDictValueType") """A type variable for a component's dictionary values.""" GraphDictKeyType = TypeVar("GraphDictKeyType", bound=Hashable) """A type variable for a graph's dictionary keys.""" GraphDictValueType = TypeVar("GraphDictValueType") """A type variable for a graph's dictionary values.""" @export class GraphException(ToolingException): """Base exception of all exceptions raised by :mod:`pyTooling.Graph`.""" @export class InternalError(GraphException): """ The exception is raised when a data structure corruption is detected. .. danger:: This exception should never be raised. If so, please create an issue at GitHub so the data structure corruption can be investigated and fixed. |br| `⇒ Bug Tracker at GitHub `__ """ @export class NotInSameGraph(GraphException): """The exception is raised when creating an edge between two vertices, but these are not in the same graph.""" @export class DuplicateVertexError(GraphException): """The exception is raised when the vertex already exists in the graph.""" @export class DuplicateEdgeError(GraphException): """The exception is raised when the edge already exists in the graph.""" @export class DestinationNotReachable(GraphException): """The exception is raised when a destination vertex is not reachable.""" @export class NotATreeError(GraphException): """ The exception is raised when a subgraph is not a tree. Either the subgraph has a cycle (backward edge) or links between branches (cross-edge). """ @export class CycleError(GraphException): """The exception is raised when a not permitted cycle is found.""" @export class Base( Generic[DictKeyType, DictValueType], metaclass=ExtendedType, slots=True ): _dict: Dict[DictKeyType, DictValueType] #: A dictionary to store arbitrary key-value-pairs. def __init__( self, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::Base::init Needs documentation. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ self._dict = {key: value for key, value in keyValuePairs.items()} if keyValuePairs is not None else {} def __del__(self) -> None: """ .. todo:: GRAPH::Base::del Needs documentation. """ try: del self._dict except AttributeError: pass def Delete(self) -> None: self._dict = None def __getitem__(self, key: DictKeyType) -> DictValueType: """ Read a vertex's attached attributes (key-value-pairs) by key. :param key: The key to look for. :returns: The value associated to the given key. """ return self._dict[key] def __setitem__(self, key: DictKeyType, value: DictValueType) -> None: """ Create or update a vertex's attached attributes (key-value-pairs) by key. If a key doesn't exist yet, a new key-value-pair is created. :param key: The key to create or update. :param value: The value to associate to the given key. """ self._dict[key] = value def __delitem__(self, key: DictKeyType) -> None: """ Remove an entry from vertex's attached attributes (key-value-pairs) by key. :param key: The key to remove. :raises KeyError: If key doesn't exist in the vertex's attributes. """ del self._dict[key] def __contains__(self, key: DictKeyType) -> bool: """ Checks if the key is an attached attribute (key-value-pairs) on this vertex. :param key: The key to check. :returns: ``True``, if the key is an attached attribute. """ return key in self._dict def __len__(self) -> int: """ Returns the number of attached attributes (key-value-pairs) on this vertex. :returns: Number of attached attributes. """ return len(self._dict) @export class BaseWithIDValueAndWeight( Base[DictKeyType, DictValueType], Generic[IDType, ValueType, WeightType, DictKeyType, DictValueType] ): _id: Nullable[IDType] #: Field storing the object's Identifier. _value: Nullable[ValueType] #: Field storing the object's value of any type. _weight: Nullable[WeightType] #: Field storing the object's weight. def __init__( self, identifier: Nullable[IDType] = None, value: Nullable[ValueType] = None, weight: Nullable[WeightType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::Vertex::init Needs documentation. :param identifier: The optional unique ID. :param value: The optional value. :param weight: The optional weight. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ super().__init__(keyValuePairs) self._id = identifier self._value = value self._weight = weight @readonly def ID(self) -> Nullable[IDType]: """ Read-only property to access the unique ID (:attr:`_id`). If no ID was given at creation time, ID returns ``None``. :returns: Unique ID, if ID was given at creation time, else ``None``. """ return self._id @property def Value(self) -> ValueType: """ Property to get and set the value (:attr:`_value`). :returns: The value. """ return self._value @Value.setter def Value(self, value: ValueType) -> None: self._value = value @property def Weight(self) -> Nullable[EdgeWeightType]: """ Property to get and set the weight (:attr:`_weight`) of an edge. :returns: The weight of an edge. """ return self._weight @Weight.setter def Weight(self, value: Nullable[EdgeWeightType]) -> None: self._weight = value @export class BaseWithName( Base[DictKeyType, DictValueType], Generic[DictKeyType, DictValueType] ): _name: Nullable[str] #: Field storing the object's name. def __init__( self, name: Nullable[str] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, ) -> None: """ .. todo:: GRAPH::BaseWithName::init Needs documentation. :param name: The optional name. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ if name is not None and not isinstance(name, str): ex = TypeError("Parameter 'name' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") raise ex super().__init__(keyValuePairs) self._name = name @property def Name(self) -> Nullable[str]: """ Property to get and set the name (:attr:`_name`). :returns: The value of a component. """ return self._name @Name.setter def Name(self, value: str) -> None: if not isinstance(value, str): ex = TypeError("Name is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex self._name = value @export class BaseWithVertices( BaseWithName[DictKeyType, DictValueType], Generic[ DictKeyType, DictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ] ): _graph: 'Graph[GraphDictKeyType, GraphDictValueType,' \ 'VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType,' \ 'EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType,' \ 'LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType' \ ']' #: Field storing a reference to the graph. _vertices: Set['Vertex[GraphDictKeyType, GraphDictValueType,' 'VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType,' 'EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType,' 'LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType' ']'] #: Field storing a set of vertices. def __init__( self, graph: 'Graph', name: Nullable[str] = None, vertices: Nullable[Iterable['Vertex']] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::Component::init Needs documentation. :param graph: The reference to the graph. :param name: The optional name. :param vertices: The optional list of vertices. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ if graph is None: raise ValueError("Parameter 'graph' is None.") elif not isinstance(graph, Graph): ex = TypeError("Parameter 'graph' is not of type 'Graph'.") ex.add_note(f"Got type '{getFullyQualifiedName(graph)}'.") raise ex super().__init__(name, keyValuePairs) self._graph = graph self._vertices = set() if vertices is None else {v for v in vertices} def __del__(self) -> None: """ .. todo:: GRAPH::BaseWithVertices::del Needs documentation. """ try: del self._vertices except AttributeError: pass super().__del__() @readonly def Graph(self) -> 'Graph': """ Read-only property to access the graph, this object is associated to (:attr:`_graph`). :returns: The graph this object is associated to. """ return self._graph @readonly def Vertices(self) -> Set['Vertex']: """ Read-only property to access the vertices in this component (:attr:`_vertices`). :returns: The set of vertices in this component. """ return self._vertices @readonly def VertexCount(self) -> int: """ Read-only property to access the number of vertices referenced by this object. :returns: The number of vertices this object references. """ return len(self._vertices) @export class Vertex( BaseWithIDValueAndWeight[VertexIDType, VertexValueType, VertexWeightType, VertexDictKeyType, VertexDictValueType], Generic[ GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ] ): """ A **vertex** can have a unique ID, a value and attached meta information as key-value-pairs. A vertex has references to inbound and outbound edges, thus a graph can be traversed in reverse. """ _graph: 'BaseGraph[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]' #: Field storing a reference to the graph. _subgraph: 'Subgraph[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]' #: Field storing a reference to the subgraph. _component: 'Component' _views: Dict[Hashable, 'View'] _inboundEdges: List['Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]'] #: Field storing a list of inbound edges. _outboundEdges: List['Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]'] #: Field storing a list of outbound edges. _inboundLinks: List['Link[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]'] #: Field storing a list of inbound links. _outboundLinks: List['Link[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]'] #: Field storing a list of outbound links. def __init__( self, vertexID: Nullable[VertexIDType] = None, value: Nullable[VertexValueType] = None, weight: Nullable[VertexWeightType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, graph: Nullable['Graph'] = None, subgraph: Nullable['Subgraph'] = None ) -> None: """ .. todo:: GRAPH::Vertex::init Needs documentation. :param vertexID: The optional ID for the new vertex. :param value: The optional value for the new vertex. :param weight: The optional weight for the new vertex. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. :param graph: The optional reference to the graph. :param subgraph: undocumented """ if vertexID is not None and not isinstance(vertexID, Hashable): ex = TypeError("Parameter 'vertexID' is not of type 'VertexIDType'.") ex.add_note(f"Got type '{getFullyQualifiedName(vertexID)}'.") raise ex super().__init__(vertexID, value, weight, keyValuePairs) if subgraph is None: self._graph = graph if graph is not None else Graph() self._subgraph = None self._component = Component(self._graph, vertices=(self,)) if vertexID is None: self._graph._verticesWithoutID.append(self) elif vertexID not in self._graph._verticesWithID: self._graph._verticesWithID[vertexID] = self else: raise DuplicateVertexError(f"Vertex ID '{vertexID}' already exists in this graph.") else: self._graph = subgraph._graph self._subgraph = subgraph self._component = Component(self._graph, vertices=(self,)) if vertexID is None: subgraph._verticesWithoutID.append(self) elif vertexID not in subgraph._verticesWithID: subgraph._verticesWithID[vertexID] = self else: raise DuplicateVertexError(f"Vertex ID '{vertexID}' already exists in this subgraph.") self._views = {} self._inboundEdges = [] self._outboundEdges = [] self._inboundLinks = [] self._outboundLinks = [] def __del__(self) -> None: """ .. todo:: GRAPH::BaseEdge::del Needs documentation. """ try: del self._views del self._inboundEdges del self._outboundEdges del self._inboundLinks del self._outboundLinks except AttributeError: pass super().__del__() def Delete(self) -> None: for edge in self._outboundEdges: edge._destination._inboundEdges.remove(edge) edge._Delete() for edge in self._inboundEdges: edge._source._outboundEdges.remove(edge) edge._Delete() for link in self._outboundLinks: link._destination._inboundLinks.remove(link) link._Delete() for link in self._inboundLinks: link._source._outboundLinks.remove(link) link._Delete() if self._id is None: self._graph._verticesWithoutID.remove(self) else: del self._graph._verticesWithID[self._id] # subgraph # component # views self._views = None self._inboundEdges = None self._outboundEdges = None self._inboundLinks = None self._outboundLinks = None super().Delete() assert getrefcount(self) == 1 @readonly def Graph(self) -> 'Graph': """ Read-only property to access the graph, this vertex is associated to (:attr:`_graph`). :returns: The graph this vertex is associated to. """ return self._graph @readonly def Component(self) -> 'Component': """ Read-only property to access the component, this vertex is associated to (:attr:`_component`). :returns: The component this vertex is associated to. """ return self._component @readonly def InboundEdges(self) -> Tuple['Edge', ...]: """ Read-only property to get a tuple of inbound edges (:attr:`_inboundEdges`). :returns: Tuple of inbound edges. """ return tuple(self._inboundEdges) @readonly def OutboundEdges(self) -> Tuple['Edge', ...]: """ Read-only property to get a tuple of outbound edges (:attr:`_outboundEdges`). :returns: Tuple of outbound edges. """ return tuple(self._outboundEdges) @readonly def InboundLinks(self) -> Tuple['Link', ...]: """ Read-only property to get a tuple of inbound links (:attr:`_inboundLinks`). :returns: Tuple of inbound links. """ return tuple(self._inboundLinks) @readonly def OutboundLinks(self) -> Tuple['Link', ...]: """ Read-only property to get a tuple of outbound links (:attr:`_outboundLinks`). :returns: Tuple of outbound links. """ return tuple(self._outboundLinks) @readonly def EdgeCount(self) -> int: """ Read-only property to get the number of all edges (inbound and outbound). :returns: Number of inbound and outbound edges. """ return len(self._inboundEdges) + len(self._outboundEdges) @readonly def InboundEdgeCount(self) -> int: """ Read-only property to get the number of inbound edges. :returns: Number of inbound edges. """ return len(self._inboundEdges) @readonly def OutboundEdgeCount(self) -> int: """ Read-only property to get the number of outbound edges. :returns: Number of outbound edges. """ return len(self._outboundEdges) @readonly def LinkCount(self) -> int: """ Read-only property to get the number of all links (inbound and outbound). :returns: Number of inbound and outbound links. """ return len(self._inboundLinks) + len(self._outboundLinks) @readonly def InboundLinkCount(self) -> int: """ Read-only property to get the number of inbound links. :returns: Number of inbound links. """ return len(self._inboundLinks) @readonly def OutboundLinkCount(self) -> int: """ Read-only property to get the number of outbound links. :returns: Number of outbound links. """ return len(self._outboundLinks) @readonly def IsRoot(self) -> bool: """ Read-only property to check if this vertex is a root vertex in the graph. A root has no inbound edges (no predecessor vertices). :returns: ``True``, if this vertex is a root. .. seealso:: :meth:`IsLeaf` |br| |rarr| Check if a vertex is a leaf vertex in the graph. :meth:`Graph.IterateRoots ` |br| |rarr| Iterate all roots of a graph. :meth:`Graph.IterateLeafs ` |br| |rarr| Iterate all leafs of a graph. """ return len(self._inboundEdges) == 0 @readonly def IsLeaf(self) -> bool: """ Read-only property to check if this vertex is a leaf vertex in the graph. A leaf has no outbound edges (no successor vertices). :returns: ``True``, if this vertex is a leaf. .. seealso:: :meth:`IsRoot` |br| |rarr| Check if a vertex is a root vertex in the graph. :meth:`Graph.IterateRoots ` |br| |rarr| Iterate all roots of a graph. :meth:`Graph.IterateLeafs ` |br| |rarr| Iterate all leafs of a graph. """ return len(self._outboundEdges) == 0 @readonly def Predecessors(self) -> Tuple['Vertex', ...]: """ Read-only property to get a tuple of predecessor vertices. :returns: Tuple of predecessor vertices. """ return tuple([edge.Source for edge in self._inboundEdges]) @readonly def Successors(self) -> Tuple['Vertex', ...]: """ Read-only property to get a tuple of successor vertices. :returns: Tuple of successor vertices. """ return tuple([edge.Destination for edge in self._outboundEdges]) def EdgeToVertex( self, vertex: 'Vertex', edgeID: Nullable[EdgeIDType] = None, edgeWeight: Nullable[EdgeWeightType] = None, edgeValue: Nullable[VertexValueType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> 'Edge': """ Create an outbound edge from this vertex to the referenced vertex. :param vertex: The vertex to be linked to. :param edgeID: The edge's optional ID for the new edge object. :param edgeWeight: The edge's optional weight for the new edge object. :param edgeValue: The edge's optional value for the new edge object. :param keyValuePairs: An optional mapping (dictionary) of key-value-pairs for the new edge object. :returns: The edge object linking this vertex and the referenced vertex. .. seealso:: :meth:`EdgeFromVertex` |br| |rarr| Create an inbound edge from the referenced vertex to this vertex. :meth:`EdgeToNewVertex` |br| |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. :meth:`EdgeFromNewVertex` |br| |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. :meth:`LinkToVertex` |br| |rarr| Create an outbound link from this vertex to the referenced vertex. :meth:`LinkFromVertex` |br| |rarr| Create an inbound link from the referenced vertex to this vertex. .. todo:: GRAPH::Vertex::EdgeToVertex Needs possible exceptions to be documented. """ if self._subgraph is vertex._subgraph: edge = Edge(self, vertex, edgeID, edgeValue, edgeWeight, keyValuePairs) self._outboundEdges.append(edge) vertex._inboundEdges.append(edge) if self._subgraph is None: # TODO: move into Edge? # TODO: keep _graph pointer in edge and then register edge on graph? if edgeID is None: self._graph._edgesWithoutID.append(edge) elif edgeID not in self._graph._edgesWithID: self._graph._edgesWithID[edgeID] = edge else: raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.") else: # TODO: keep _graph pointer in edge and then register edge on graph? if edgeID is None: self._subgraph._edgesWithoutID.append(edge) elif edgeID not in self._subgraph._edgesWithID: self._subgraph._edgesWithID[edgeID] = edge else: raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this subgraph.") else: # FIXME: needs an error message raise GraphException() return edge def EdgeFromVertex( self, vertex: 'Vertex', edgeID: Nullable[EdgeIDType] = None, edgeWeight: Nullable[EdgeWeightType] = None, edgeValue: Nullable[VertexValueType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> 'Edge': """ Create an inbound edge from the referenced vertex to this vertex. :param vertex: The vertex to be linked from. :param edgeID: The edge's optional ID for the new edge object. :param edgeWeight: The edge's optional weight for the new edge object. :param edgeValue: The edge's optional value for the new edge object. :param keyValuePairs: An optional mapping (dictionary) of key-value-pairs for the new edge object. :returns: The edge object linking the referenced vertex and this vertex. .. seealso:: :meth:`EdgeToVertex` |br| |rarr| Create an outbound edge from this vertex to the referenced vertex. :meth:`EdgeToNewVertex` |br| |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. :meth:`EdgeFromNewVertex` |br| |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. :meth:`LinkToVertex` |br| |rarr| Create an outbound link from this vertex to the referenced vertex. :meth:`LinkFromVertex` |br| |rarr| Create an inbound link from the referenced vertex to this vertex. .. todo:: GRAPH::Vertex::EdgeFromVertex Needs possible exceptions to be documented. """ if self._subgraph is vertex._subgraph: edge = Edge(vertex, self, edgeID, edgeValue, edgeWeight, keyValuePairs) vertex._outboundEdges.append(edge) self._inboundEdges.append(edge) if self._subgraph is None: # TODO: move into Edge? # TODO: keep _graph pointer in edge and then register edge on graph? if edgeID is None: self._graph._edgesWithoutID.append(edge) elif edgeID not in self._graph._edgesWithID: self._graph._edgesWithID[edgeID] = edge else: raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.") else: # TODO: keep _graph pointer in edge and then register edge on graph? if edgeID is None: self._subgraph._edgesWithoutID.append(edge) elif edgeID not in self._graph._edgesWithID: self._subgraph._edgesWithID[edgeID] = edge else: raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.") else: # FIXME: needs an error message raise GraphException() return edge def EdgeToNewVertex( self, vertexID: Nullable[VertexIDType] = None, vertexValue: Nullable[VertexValueType] = None, vertexWeight: Nullable[VertexWeightType] = None, vertexKeyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, edgeID: Nullable[EdgeIDType] = None, edgeWeight: Nullable[EdgeWeightType] = None, edgeValue: Nullable[VertexValueType] = None, edgeKeyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> 'Edge': """ Create a new vertex and link that vertex by an outbound edge from this vertex. :param vertexID: The new vertex' optional ID. :param vertexValue: The new vertex' optional value. :param vertexWeight: The new vertex' optional weight. :param vertexKeyValuePairs: An optional mapping (dictionary) of key-value-pairs for the new vertex. :param edgeID: The edge's optional ID for the new edge object. :param edgeWeight: The edge's optional weight for the new edge object. :param edgeValue: The edge's optional value for the new edge object. :param edgeKeyValuePairs: An optional mapping (dictionary) of key-value-pairs for the new edge object. :returns: The edge object linking this vertex and the created vertex. .. seealso:: :meth:`EdgeToVertex` |br| |rarr| Create an outbound edge from this vertex to the referenced vertex. :meth:`EdgeFromVertex` |br| |rarr| Create an inbound edge from the referenced vertex to this vertex. :meth:`EdgeFromNewVertex` |br| |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. :meth:`LinkToVertex` |br| |rarr| Create an outbound link from this vertex to the referenced vertex. :meth:`LinkFromVertex` |br| |rarr| Create an inbound link from the referenced vertex to this vertex. .. todo:: GRAPH::Vertex::EdgeToNewVertex Needs possible exceptions to be documented. """ vertex = Vertex(vertexID, vertexValue, vertexWeight, vertexKeyValuePairs, graph=self._graph) # , component=self._component) if self._subgraph is vertex._subgraph: edge = Edge(self, vertex, edgeID, edgeValue, edgeWeight, edgeKeyValuePairs) self._outboundEdges.append(edge) vertex._inboundEdges.append(edge) if self._subgraph is None: # TODO: move into Edge? # TODO: keep _graph pointer in edge and then register edge on graph? if edgeID is None: self._graph._edgesWithoutID.append(edge) elif edgeID not in self._graph._edgesWithID: self._graph._edgesWithID[edgeID] = edge else: raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.") else: # TODO: keep _graph pointer in edge and then register edge on graph? if edgeID is None: self._subgraph._edgesWithoutID.append(edge) elif edgeID not in self._graph._edgesWithID: self._subgraph._edgesWithID[edgeID] = edge else: raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.") else: # FIXME: needs an error message raise GraphException() return edge def EdgeFromNewVertex( self, vertexID: Nullable[VertexIDType] = None, vertexValue: Nullable[VertexValueType] = None, vertexWeight: Nullable[VertexWeightType] = None, vertexKeyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, edgeID: Nullable[EdgeIDType] = None, edgeWeight: Nullable[EdgeWeightType] = None, edgeValue: Nullable[VertexValueType] = None, edgeKeyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> 'Edge': """ Create a new vertex and link that vertex by an inbound edge to this vertex. :param vertexID: The new vertex' optional ID. :param vertexValue: The new vertex' optional value. :param vertexWeight: The new vertex' optional weight. :param vertexKeyValuePairs: An optional mapping (dictionary) of key-value-pairs for the new vertex. :param edgeID: The edge's optional ID for the new edge object. :param edgeWeight: The edge's optional weight for the new edge object. :param edgeValue: The edge's optional value for the new edge object. :param edgeKeyValuePairs: An optional mapping (dictionary) of key-value-pairs for the new edge object. :returns: The edge object linking this vertex and the created vertex. .. seealso:: :meth:`EdgeToVertex` |br| |rarr| Create an outbound edge from this vertex to the referenced vertex. :meth:`EdgeFromVertex` |br| |rarr| Create an inbound edge from the referenced vertex to this vertex. :meth:`EdgeToNewVertex` |br| |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. :meth:`LinkToVertex` |br| |rarr| Create an outbound link from this vertex to the referenced vertex. :meth:`LinkFromVertex` |br| |rarr| Create an inbound link from the referenced vertex to this vertex. .. todo:: GRAPH::Vertex::EdgeFromNewVertex Needs possible exceptions to be documented. """ vertex = Vertex(vertexID, vertexValue, vertexWeight, vertexKeyValuePairs, graph=self._graph) # , component=self._component) if self._subgraph is vertex._subgraph: edge = Edge(vertex, self, edgeID, edgeValue, edgeWeight, edgeKeyValuePairs) vertex._outboundEdges.append(edge) self._inboundEdges.append(edge) if self._subgraph is None: # TODO: move into Edge? # TODO: keep _graph pointer in edge and then register edge on graph? if edgeID is None: self._graph._edgesWithoutID.append(edge) elif edgeID not in self._graph._edgesWithID: self._graph._edgesWithID[edgeID] = edge else: raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.") else: # TODO: keep _graph pointer in edge and then register edge on graph? if edgeID is None: self._subgraph._edgesWithoutID.append(edge) elif edgeID not in self._graph._edgesWithID: self._subgraph._edgesWithID[edgeID] = edge else: raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.") else: # FIXME: needs an error message raise GraphException() return edge def LinkToVertex( self, vertex: 'Vertex', linkID: Nullable[EdgeIDType] = None, linkWeight: Nullable[EdgeWeightType] = None, linkValue: Nullable[VertexValueType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, ) -> 'Link': """ Create an outbound link from this vertex to the referenced vertex. :param vertex: The vertex to be linked to. :param edgeID: The edge's optional ID for the new link object. :param edgeWeight: The edge's optional weight for the new link object. :param edgeValue: The edge's optional value for the new link object. :param keyValuePairs: An optional mapping (dictionary) of key-value-pairs for the new link object. :returns: The link object linking this vertex and the referenced vertex. .. seealso:: :meth:`EdgeToVertex` |br| |rarr| Create an outbound edge from this vertex to the referenced vertex. :meth:`EdgeFromVertex` |br| |rarr| Create an inbound edge from the referenced vertex to this vertex. :meth:`EdgeToNewVertex` |br| |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. :meth:`EdgeFromNewVertex` |br| |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. :meth:`LinkFromVertex` |br| |rarr| Create an inbound link from the referenced vertex to this vertex. .. todo:: GRAPH::Vertex::LinkToVertex Needs possible exceptions to be documented. """ if self._subgraph is vertex._subgraph: # FIXME: needs an error message raise GraphException() else: link = Link(self, vertex, linkID, linkValue, linkWeight, keyValuePairs) self._outboundLinks.append(link) vertex._inboundLinks.append(link) if self._subgraph is None: # TODO: move into Edge? # TODO: keep _graph pointer in link and then register link on graph? if linkID is None: self._graph._linksWithoutID.append(link) elif linkID not in self._graph._linksWithID: self._graph._linksWithID[linkID] = link else: raise DuplicateEdgeError(f"Link ID '{linkID}' already exists in this graph.") else: # TODO: keep _graph pointer in link and then register link on graph? if linkID is None: self._subgraph._linksWithoutID.append(link) vertex._subgraph._linksWithoutID.append(link) elif linkID not in self._graph._linksWithID: self._subgraph._linksWithID[linkID] = link vertex._subgraph._linksWithID[linkID] = link else: raise DuplicateEdgeError(f"Link ID '{linkID}' already exists in this graph.") return link def LinkFromVertex( self, vertex: 'Vertex', linkID: Nullable[EdgeIDType] = None, linkWeight: Nullable[EdgeWeightType] = None, linkValue: Nullable[VertexValueType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> 'Edge': """ Create an inbound link from the referenced vertex to this vertex. :param vertex: The vertex to be linked from. :param edgeID: The edge's optional ID for the new link object. :param edgeWeight: The edge's optional weight for the new link object. :param edgeValue: The edge's optional value for the new link object. :param keyValuePairs: An optional mapping (dictionary) of key-value-pairs for the new link object. :returns: The link object linking the referenced vertex and this vertex. .. seealso:: :meth:`EdgeToVertex` |br| |rarr| Create an outbound edge from this vertex to the referenced vertex. :meth:`EdgeFromVertex` |br| |rarr| Create an inbound edge from the referenced vertex to this vertex. :meth:`EdgeToNewVertex` |br| |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. :meth:`EdgeFromNewVertex` |br| |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. :meth:`LinkToVertex` |br| |rarr| Create an outbound link from this vertex to the referenced vertex. .. todo:: GRAPH::Vertex::LinkFromVertex Needs possible exceptions to be documented. """ if self._subgraph is vertex._subgraph: # FIXME: needs an error message raise GraphException() else: link = Link(vertex, self, linkID, linkValue, linkWeight, keyValuePairs) vertex._outboundLinks.append(link) self._inboundLinks.append(link) if self._subgraph is None: # TODO: move into Edge? # TODO: keep _graph pointer in link and then register link on graph? if linkID is None: self._graph._linksWithoutID.append(link) elif linkID not in self._graph._linksWithID: self._graph._linksWithID[linkID] = link else: raise DuplicateEdgeError(f"Link ID '{linkID}' already exists in this graph.") else: # TODO: keep _graph pointer in link and then register link on graph? if linkID is None: self._subgraph._linksWithoutID.append(link) vertex._subgraph._linksWithoutID.append(link) elif linkID not in self._graph._linksWithID: self._subgraph._linksWithID[linkID] = link vertex._subgraph._linksWithID[linkID] = link else: raise DuplicateEdgeError(f"Link ID '{linkID}' already exists in this graph.") return link def HasEdgeToDestination(self, destination: 'Vertex') -> bool: """ Check if this vertex is linked to another vertex by any outbound edge. :param destination: Destination vertex to check. :returns: ``True``, if the destination vertex is a destination on any outbound edge. .. seealso:: :meth:`HasEdgeFromSource` |br| |rarr| Check if this vertex is linked to another vertex by any inbound edge. :meth:`HasLinkToDestination` |br| |rarr| Check if this vertex is linked to another vertex by any outbound link. :meth:`HasLinkFromSource` |br| |rarr| Check if this vertex is linked to another vertex by any inbound link. """ for edge in self._outboundEdges: if destination is edge.Destination: return True return False def HasEdgeFromSource(self, source: 'Vertex') -> bool: """ Check if this vertex is linked to another vertex by any inbound edge. :param source: Source vertex to check. :returns: ``True``, if the source vertex is a source on any inbound edge. .. seealso:: :meth:`HasEdgeToDestination` |br| |rarr| Check if this vertex is linked to another vertex by any outbound edge. :meth:`HasLinkToDestination` |br| |rarr| Check if this vertex is linked to another vertex by any outbound link. :meth:`HasLinkFromSource` |br| |rarr| Check if this vertex is linked to another vertex by any inbound link. """ for edge in self._inboundEdges: if source is edge.Source: return True return False def HasLinkToDestination(self, destination: 'Vertex') -> bool: """ Check if this vertex is linked to another vertex by any outbound link. :param destination: Destination vertex to check. :returns: ``True``, if the destination vertex is a destination on any outbound link. .. seealso:: :meth:`HasEdgeToDestination` |br| |rarr| Check if this vertex is linked to another vertex by any outbound edge. :meth:`HasEdgeFromSource` |br| |rarr| Check if this vertex is linked to another vertex by any inbound edge. :meth:`HasLinkFromSource` |br| |rarr| Check if this vertex is linked to another vertex by any inbound link. """ for link in self._outboundLinks: if destination is link.Destination: return True return False def HasLinkFromSource(self, source: 'Vertex') -> bool: """ Check if this vertex is linked to another vertex by any inbound link. :param source: Source vertex to check. :returns: ``True``, if the source vertex is a source on any inbound link. .. seealso:: :meth:`HasEdgeToDestination` |br| |rarr| Check if this vertex is linked to another vertex by any outbound edge. :meth:`HasEdgeFromSource` |br| |rarr| Check if this vertex is linked to another vertex by any inbound edge. :meth:`HasLinkToDestination` |br| |rarr| Check if this vertex is linked to another vertex by any outbound link. """ for link in self._inboundLinks: if source is link.Source: return True return False def DeleteEdgeTo(self, destination: 'Vertex') -> None: for edge in self._outboundEdges: if edge._destination is destination: break else: raise GraphException(f"No outbound edge found to '{destination!r}'.") edge.Delete() def DeleteEdgeFrom(self, source: 'Vertex') -> None: for edge in self._inboundEdges: if edge._source is source: break else: raise GraphException(f"No inbound edge found to '{source!r}'.") edge.Delete() def DeleteLinkTo(self, destination: 'Vertex') -> None: for link in self._outboundLinks: if link._destination is destination: break else: raise GraphException(f"No outbound link found to '{destination!r}'.") link.Delete() def DeleteLinkFrom(self, source: 'Vertex') -> None: for link in self._inboundLinks: if link._source is source: break else: raise GraphException(f"No inbound link found to '{source!r}'.") link.Delete() def Copy(self, graph: Graph, copyDict: bool = False, linkingKeyToOriginalVertex: Nullable[str] = None, linkingKeyFromOriginalVertex: Nullable[str] = None) -> 'Vertex': """ Creates a copy of this vertex in another graph. Optionally, the vertex's attached attributes (key-value-pairs) can be copied and a linkage between both vertices can be established. :param graph: The graph, the vertex is created in. :param copyDict: If ``True``, copy all attached attributes into the new vertex. :param linkingKeyToOriginalVertex: If not ``None``, add a key-value-pair using this parameter as key from new vertex to the original vertex. :param linkingKeyFromOriginalVertex: If not ``None``, add a key-value-pair using this parameter as key from original vertex to the new vertex. :returns: The newly created vertex. :raises GraphException: If source graph and destination graph are the same. """ if graph is self._graph: raise GraphException("Graph to copy this vertex to, is the same graph.") vertex = Vertex(self._id, self._value, self._weight, graph=graph) if copyDict: vertex._dict = self._dict.copy() if linkingKeyToOriginalVertex is not None: vertex._dict[linkingKeyToOriginalVertex] = self if linkingKeyFromOriginalVertex is not None: self._dict[linkingKeyFromOriginalVertex] = vertex return vertex def IterateOutboundEdges(self, predicate: Nullable[Callable[['Edge'], bool]] = None) -> Generator['Edge', None, None]: """ Iterate all or selected outbound edges of this vertex. If parameter ``predicate`` is not None, the given filter function is used to skip edges in the generator. :param predicate: Filter function accepting any edge and returning a boolean. :returns: A generator to iterate all outbound edges. """ if predicate is None: for edge in self._outboundEdges: yield edge else: for edge in self._outboundEdges: if predicate(edge): yield edge def IterateInboundEdges(self, predicate: Nullable[Callable[['Edge'], bool]] = None) -> Generator['Edge', None, None]: """ Iterate all or selected inbound edges of this vertex. If parameter ``predicate`` is not None, the given filter function is used to skip edges in the generator. :param predicate: Filter function accepting any edge and returning a boolean. :returns: A generator to iterate all inbound edges. """ if predicate is None: for edge in self._inboundEdges: yield edge else: for edge in self._inboundEdges: if predicate(edge): yield edge def IterateOutboundLinks(self, predicate: Nullable[Callable[['Link'], bool]] = None) -> Generator['Link', None, None]: """ Iterate all or selected outbound links of this vertex. If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator. :param predicate: Filter function accepting any link and returning a boolean. :returns: A generator to iterate all outbound links. """ if predicate is None: for link in self._outboundLinks: yield link else: for link in self._outboundLinks: if predicate(link): yield link def IterateInboundLinks(self, predicate: Nullable[Callable[['Link'], bool]] = None) -> Generator['Link', None, None]: """ Iterate all or selected inbound links of this vertex. If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator. :param predicate: Filter function accepting any link and returning a boolean. :returns: A generator to iterate all inbound links. """ if predicate is None: for link in self._inboundLinks: yield link else: for link in self._inboundLinks: if predicate(link): yield link def IterateSuccessorVertices(self, predicate: Nullable[Callable[['Edge'], bool]] = None) -> Generator['Vertex', None, None]: """ Iterate all or selected successor vertices of this vertex. If parameter ``predicate`` is not None, the given filter function is used to skip successors in the generator. :param predicate: Filter function accepting any edge and returning a boolean. :returns: A generator to iterate all successor vertices. """ if predicate is None: for edge in self._outboundEdges: yield edge.Destination else: for edge in self._outboundEdges: if predicate(edge): yield edge.Destination def IteratePredecessorVertices(self, predicate: Nullable[Callable[['Edge'], bool]] = None) -> Generator['Vertex', None, None]: """ Iterate all or selected predecessor vertices of this vertex. If parameter ``predicate`` is not None, the given filter function is used to skip predecessors in the generator. :param predicate: Filter function accepting any edge and returning a boolean. :returns: A generator to iterate all predecessor vertices. """ if predicate is None: for edge in self._inboundEdges: yield edge.Source else: for edge in self._inboundEdges: if predicate(edge): yield edge.Source def IterateVerticesBFS(self) -> Generator['Vertex', None, None]: """ A generator to iterate all reachable vertices starting from this node in breadth-first search (BFS) order. :returns: A generator to iterate vertices traversed in BFS order. .. seealso:: :meth:`IterateVerticesDFS` |br| |rarr| Iterate all reachable vertices **depth-first search** order. """ visited: Set[Vertex] = set() queue: Deque[Vertex] = deque() yield self visited.add(self) for edge in self._outboundEdges: nextVertex = edge.Destination if nextVertex is not self: queue.appendleft(nextVertex) visited.add(nextVertex) while queue: vertex = queue.pop() yield vertex for edge in vertex._outboundEdges: nextVertex = edge.Destination if nextVertex not in visited: queue.appendleft(nextVertex) visited.add(nextVertex) def IterateVerticesDFS(self) -> Generator['Vertex', None, None]: """ A generator to iterate all reachable vertices starting from this node in depth-first search (DFS) order. :returns: A generator to iterate vertices traversed in DFS order. .. seealso:: :meth:`IterateVerticesBFS` |br| |rarr| Iterate all reachable vertices **breadth-first search** order. Wikipedia - https://en.wikipedia.org/wiki/Depth-first_search """ visited: Set[Vertex] = set() stack: List[typing_Iterator[Edge]] = list() yield self visited.add(self) stack.append(iter(self._outboundEdges)) while True: try: edge = next(stack[-1]) nextVertex = edge._destination if nextVertex not in visited: visited.add(nextVertex) yield nextVertex if len(nextVertex._outboundEdges) != 0: stack.append(iter(nextVertex._outboundEdges)) except StopIteration: stack.pop() if len(stack) == 0: return def IterateAllOutboundPathsAsVertexList(self) -> Generator[Tuple['Vertex', ...], None, None]: if len(self._outboundEdges) == 0: yield (self, ) return visited: Set[Vertex] = set() vertexStack: List[Vertex] = list() iteratorStack: List[typing_Iterator[Edge]] = list() visited.add(self) vertexStack.append(self) iteratorStack.append(iter(self._outboundEdges)) while True: try: edge = next(iteratorStack[-1]) nextVertex = edge._destination if nextVertex in visited: ex = CycleError(f"Loop detected.") ex.add_note(f"First loop is:") for i, vertex in enumerate(vertexStack): ex.add_note(f" {i}: {vertex!r}") raise ex vertexStack.append(nextVertex) if len(nextVertex._outboundEdges) == 0: yield tuple(vertexStack) vertexStack.pop() else: iteratorStack.append(iter(nextVertex._outboundEdges)) except StopIteration: vertexStack.pop() iteratorStack.pop() if len(vertexStack) == 0: return def ShortestPathToByHops(self, destination: 'Vertex') -> Generator['Vertex', None, None]: """ Compute the shortest path (by hops) between this vertex and the destination vertex. A generator is return to iterate all vertices along the path including source and destination vertex. The search algorithm is breadth-first search (BFS) based. The found solution, if any, is not unique but deterministic as long as the graph was not modified (e.g. ordering of edges on vertices). :param destination: The destination vertex to reach. :returns: A generator to iterate all vertices on the path found between this vertex and the destination vertex. """ # Trivial case if start is destination if self is destination: yield self return # Local struct to create multiple linked-lists forming a paths from current node back to the starting point # (actually a tree). Each node holds a reference to the vertex it represents. # Hint: slotted classes are faster than '@dataclasses.dataclass'. class Node(metaclass=ExtendedType, slots=True): parent: 'Node' ref: Vertex def __init__(self, parent: 'Node', ref: Vertex) -> None: self.parent = parent self.ref = ref def __str__(self): return f"Vertex: {self.ref.ID}" # Initially add all reachable vertices to a queue if vertices to be processed. startNode = Node(None, self) visited: Set[Vertex] = set() queue: Deque[Node] = deque() # Add starting vertex and all its children to the processing list. # If a child is the destination, break immediately else go into 'else' branch and use BFS algorithm. visited.add(self) for edge in self._outboundEdges: nextVertex = edge.Destination if nextVertex is destination: # Child is destination, so construct the last node for path traversal and break from loop. destinationNode = Node(startNode, nextVertex) break if nextVertex is not self: # Ignore backward-edges and side-edges. # Here self-edges, because there is only the starting vertex in the list of visited edges. visited.add(nextVertex) queue.appendleft(Node(startNode, nextVertex)) else: # Process queue until destination is found or no further vertices are reachable. while queue: node = queue.pop() for edge in node.ref._outboundEdges: nextVertex = edge.Destination # Next reachable vertex is destination, so construct the last node for path traversal and break from loop. if nextVertex is destination: destinationNode = Node(node, nextVertex) break # Ignore backward-edges and side-edges. if nextVertex not in visited: visited.add(nextVertex) queue.appendleft(Node(node, nextVertex)) # Next 3 lines realize a double-break if break was called in inner loop, otherwise continue with outer loop. else: continue break else: # All reachable vertices have been processed, but destination was not among them. raise DestinationNotReachable(f"Destination is not reachable.") # Reverse order of linked list from destinationNode to startNode currentNode = destinationNode previousNode = destinationNode.parent currentNode.parent = None while previousNode is not None: node = previousNode.parent previousNode.parent = currentNode currentNode = previousNode previousNode = node # Scan reversed linked-list and yield referenced vertices yield startNode.ref node = startNode.parent while node is not None: yield node.ref node = node.parent def ShortestPathToByWeight(self, destination: 'Vertex') -> Generator['Vertex', None, None]: """ Compute the shortest path (by edge weight) between this vertex and the destination vertex. A generator is return to iterate all vertices along the path including source and destination vertex. The search algorithm is based on Dijkstra algorithm and using :mod:`heapq`. The found solution, if any, is not unique but deterministic as long as the graph was not modified (e.g. ordering of edges on vertices). :param destination: The destination vertex to reach. :returns: A generator to iterate all vertices on the path found between this vertex and the destination vertex. """ # Improvements: both-sided Dijkstra (search from start and destination to reduce discovered area. # Trivial case if start is destination if self is destination: yield self return # Local struct to create multiple-linked lists forming a paths from current node back to the starting point # (actually a tree). Each node holds the overall weight from start to current node and a reference to the vertex it # represents. # Hint: slotted classes are faster than '@dataclasses.dataclass'. class Node(metaclass=ExtendedType, slots=True): parent: 'Node' distance: EdgeWeightType ref: Vertex def __init__(self, parent: 'Node', distance: EdgeWeightType, ref: Vertex) -> None: self.parent = parent self.distance = distance self.ref = ref def __lt__(self, other): return self.distance < other.distance def __str__(self): return f"Vertex: {self.ref.ID}" visited: Set['Vertex'] = set() startNode = Node(None, 0, self) priorityQueue = [startNode] # Add starting vertex and all its children to the processing list. # If a child is the destination, break immediately else go into 'else' branch and use Dijkstra algorithm. visited.add(self) for edge in self._outboundEdges: nextVertex = edge.Destination # Child is destination, so construct the last node for path traversal and break from loop. if nextVertex is destination: destinationNode = Node(startNode, edge._weight, nextVertex) break # Ignore backward-edges and side-edges. # Here self-edges, because there is only the starting vertex in the list of visited edges. if nextVertex is not self: visited.add(nextVertex) heapq.heappush(priorityQueue, Node(startNode, edge._weight, nextVertex)) else: # Process priority queue until destination is found or no further vertices are reachable. while priorityQueue: node = heapq.heappop(priorityQueue) for edge in node.ref._outboundEdges: nextVertex = edge.Destination # Next reachable vertex is destination, so construct the last node for path traversal and break from loop. if nextVertex is destination: destinationNode = Node(node, node.distance + edge._weight, nextVertex) break # Ignore backward-edges and side-edges. if nextVertex not in visited: visited.add(nextVertex) heapq.heappush(priorityQueue, Node(node, node.distance + edge._weight, nextVertex)) # Next 3 lines realize a double-break if break was called in inner loop, otherwise continue with outer loop. else: continue break else: # All reachable vertices have been processed, but destination was not among them. raise DestinationNotReachable(f"Destination is not reachable.") # Reverse order of linked-list from destinationNode to startNode currentNode = destinationNode previousNode = destinationNode.parent currentNode.parent = None while previousNode is not None: node = previousNode.parent previousNode.parent = currentNode currentNode = previousNode previousNode = node # Scan reversed linked-list and yield referenced vertices yield startNode.ref, startNode.distance node = startNode.parent while node is not None: yield node.ref, node.distance node = node.parent # Other possible algorithms: # * Bellman-Ford # * Floyd-Warshall # def PathExistsTo(self, destination: 'Vertex'): # raise NotImplementedError() # # DFS # # Union find # # def MaximumFlowTo(self, destination: 'Vertex'): # raise NotImplementedError() # # Ford-Fulkerson algorithm # # Edmons-Karp algorithm # # Dinic's algorithm def ConvertToTree(self) -> Node: """ Converts all reachable vertices from this starting vertex to a tree of :class:`~pyTooling.Tree.Node` instances. The tree is traversed using depths-first-search. :returns: """ visited: Set[Vertex] = set() stack: List[Tuple[Node, typing_Iterator[Edge]]] = list() root = Node(nodeID=self._id, value=self._value) root._dict = self._dict.copy() visited.add(self) stack.append((root, iter(self._outboundEdges))) while True: try: edge = next(stack[-1][1]) nextVertex = edge._destination if nextVertex not in visited: node = Node(nextVertex._id, nextVertex._value, parent=stack[-1][0]) visited.add(nextVertex) if len(nextVertex._outboundEdges) != 0: stack.append((node, iter(nextVertex._outboundEdges))) else: raise NotATreeError(f"The directed subgraph is not a tree.") # TODO: compute cycle: # a) branch 1 is described in stack # b) branch 2 can be found by walking from joint to root in the tree except StopIteration: stack.pop() if len(stack) == 0: return root def __repr__(self) -> str: """ Returns a detailed string representation of the vertex. :returns: The detailed string representation of the vertex. """ vertexID = value = "" sep = ": " if self._id is not None: vertexID = f"{sep}vertexID='{self._id}'" sep = "; " if self._value is not None: value = f"{sep}value='{self._value}'" return f"" def __str__(self) -> str: """ Return a string representation of the vertex. Order of resolution: 1. If :attr:`_value` is not None, return the string representation of :attr:`_value`. 2. If :attr:`_id` is not None, return the string representation of :attr:`_id`. 3. Else, return :meth:`__repr__`. :returns: The resolved string representation of the vertex. """ if self._value is not None: return str(self._value) elif self._id is not None: return str(self._id) else: return self.__repr__() @export class BaseEdge( BaseWithIDValueAndWeight[EdgeIDType, EdgeValueType, EdgeWeightType, EdgeDictKeyType, EdgeDictValueType], Generic[EdgeIDType, EdgeValueType, EdgeWeightType, EdgeDictKeyType, EdgeDictValueType] ): """ An **edge** can have a unique ID, a value, a weight and attached meta information as key-value-pairs. All edges are directed. """ _source: Vertex _destination: Vertex def __init__( self, source: Vertex, destination: Vertex, edgeID: Nullable[EdgeIDType] = None, value: Nullable[EdgeValueType] = None, weight: Nullable[EdgeWeightType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::BaseEdge::init Needs documentation. :param source: The source of the new edge. :param destination: The destination of the new edge. :param edgeID: The optional unique ID for the new edge. :param value: The optional value for the new edge. :param weight: The optional weight for the new edge. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ super().__init__(edgeID, value, weight, keyValuePairs) self._source = source self._destination = destination component = source._component if component is not destination._component: # TODO: should it be divided into with/without ID? oldComponent = destination._component for vertex in oldComponent._vertices: vertex._component = component component._vertices.add(vertex) component._graph._components.remove(oldComponent) del oldComponent @readonly def Source(self) -> Vertex: """ Read-only property to get the source (:attr:`_source`) of an edge. :returns: The source of an edge. """ return self._source @readonly def Destination(self) -> Vertex: """ Read-only property to get the destination (:attr:`_destination`) of an edge. :returns: The destination of an edge. """ return self._destination def Reverse(self) -> None: """Reverse the direction of this edge.""" swap = self._source self._source = self._destination self._destination = swap @export class Edge( BaseEdge[EdgeIDType, EdgeValueType, EdgeWeightType, EdgeDictKeyType, EdgeDictValueType], Generic[EdgeIDType, EdgeValueType, EdgeWeightType, EdgeDictKeyType, EdgeDictValueType] ): """ An **edge** can have a unique ID, a value, a weight and attached meta information as key-value-pairs. All edges are directed. """ def __init__( self, source: Vertex, destination: Vertex, edgeID: Nullable[EdgeIDType] = None, value: Nullable[EdgeValueType] = None, weight: Nullable[EdgeWeightType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::Edge::init Needs documentation. :param source: The source of the new edge. :param destination: The destination of the new edge. :param edgeID: The optional unique ID for the new edge. :param value: The optional value for the new edge. :param weight: The optional weight for the new edge. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ if not isinstance(source, Vertex): ex = TypeError("Parameter 'source' is not of type 'Vertex'.") ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.") raise ex if not isinstance(destination, Vertex): ex = TypeError("Parameter 'destination' is not of type 'Vertex'.") ex.add_note(f"Got type '{getFullyQualifiedName(destination)}'.") raise ex if edgeID is not None and not isinstance(edgeID, Hashable): ex = TypeError("Parameter 'edgeID' is not of type 'EdgeIDType'.") ex.add_note(f"Got type '{getFullyQualifiedName(edgeID)}'.") raise ex # if value is not None and not isinstance(value, Vertex): # raise TypeError("Parameter 'value' is not of type 'EdgeValueType'.") if weight is not None and not isinstance(weight, (int, float)): ex = TypeError("Parameter 'weight' is not of type 'EdgeWeightType'.") ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.") raise ex if source._graph is not destination._graph: raise NotInSameGraph(f"Source vertex and destination vertex are not in same graph.") super().__init__(source, destination, edgeID, value, weight, keyValuePairs) def Delete(self) -> None: # Remove from Source and Destination self._source._outboundEdges.remove(self) self._destination._inboundEdges.remove(self) # Remove from Graph and Subgraph if self._id is None: self._source._graph._edgesWithoutID.remove(self) if self._source._subgraph is not None: self._source._subgraph._edgesWithoutID.remove(self) else: del self._source._graph._edgesWithID[self._id] if self._source._subgraph is not None: del self._source._subgraph._edgesWithID[self] self._Delete() def _Delete(self) -> None: super().Delete() def Reverse(self) -> None: """Reverse the direction of this edge.""" self._source._outboundEdges.remove(self) self._source._inboundEdges.append(self) self._destination._inboundEdges.remove(self) self._destination._outboundEdges.append(self) super().Reverse() @export class Link( BaseEdge[LinkIDType, LinkValueType, LinkWeightType, LinkDictKeyType, LinkDictValueType], Generic[LinkIDType, LinkValueType, LinkWeightType, LinkDictKeyType, LinkDictValueType] ): """ A **link** can have a unique ID, a value, a weight and attached meta information as key-value-pairs. All links are directed. """ def __init__( self, source: Vertex, destination: Vertex, linkID: LinkIDType = None, value: LinkValueType = None, weight: Nullable[LinkWeightType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::Edge::init Needs documentation. :param source: The source of the new link. :param destination: The destination of the new link. :param linkID: The optional unique ID for the new link. :param value: The optional value for the new v. :param weight: The optional weight for the new link. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ if not isinstance(source, Vertex): ex = TypeError("Parameter 'source' is not of type 'Vertex'.") ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.") raise ex if not isinstance(destination, Vertex): ex = TypeError("Parameter 'destination' is not of type 'Vertex'.") ex.add_note(f"Got type '{getFullyQualifiedName(destination)}'.") raise ex if linkID is not None and not isinstance(linkID, Hashable): ex = TypeError("Parameter 'linkID' is not of type 'LinkIDType'.") ex.add_note(f"Got type '{getFullyQualifiedName(linkID)}'.") raise ex # if value is not None and not isinstance(value, Vertex): # raise TypeError("Parameter 'value' is not of type 'EdgeValueType'.") if weight is not None and not isinstance(weight, (int, float)): ex = TypeError("Parameter 'weight' is not of type 'EdgeWeightType'.") ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.") raise ex if source._graph is not destination._graph: raise NotInSameGraph(f"Source vertex and destination vertex are not in same graph.") super().__init__(source, destination, linkID, value, weight, keyValuePairs) def Delete(self) -> None: self._source._outboundEdges.remove(self) self._destination._inboundEdges.remove(self) if self._id is None: self._source._graph._linksWithoutID.remove(self) else: del self._source._graph._linksWithID[self._id] self._Delete() assert getrefcount(self) == 1 def _Delete(self) -> None: super().Delete() def Reverse(self) -> None: """Reverse the direction of this link.""" self._source._outboundEdges.remove(self) self._source._inboundEdges.append(self) self._destination._inboundEdges.remove(self) self._destination._outboundEdges.append(self) super().Reverse() @export class BaseGraph( BaseWithName[GraphDictKeyType, GraphDictValueType], Generic[ GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ] ): """ .. todo:: GRAPH::BaseGraph Needs documentation. """ _verticesWithID: Dict[VertexIDType, Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] _verticesWithoutID: List[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] _edgesWithID: Dict[EdgeIDType, Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]] _edgesWithoutID: List[Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]] _linksWithID: Dict[EdgeIDType, Link[LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] _linksWithoutID: List[Link[LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] def __init__( self, name: Nullable[str] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None #, vertices: Nullable[Iterable[Vertex]] = None) -> None: ) -> None: """ .. todo:: GRAPH::BaseGraph::init Needs documentation. :param name: The optional name of the graph. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ super().__init__(name, keyValuePairs) self._verticesWithoutID = [] self._verticesWithID = {} self._edgesWithoutID = [] self._edgesWithID = {} self._linksWithoutID = [] self._linksWithID = {} def __del__(self) -> None: """ .. todo:: GRAPH::BaseGraph::del Needs documentation. """ try: del self._verticesWithoutID del self._verticesWithID del self._edgesWithoutID del self._edgesWithID del self._linksWithoutID del self._linksWithID except AttributeError: pass super().__del__() @readonly def VertexCount(self) -> int: """Read-only property to access the number of vertices in this graph. :returns: The number of vertices in this graph.""" return len(self._verticesWithoutID) + len(self._verticesWithID) @readonly def EdgeCount(self) -> int: """Read-only property to access the number of edges in this graph. :returns: The number of edges in this graph.""" return len(self._edgesWithoutID) + len(self._edgesWithID) @readonly def LinkCount(self) -> int: """Read-only property to access the number of links in this graph. :returns: The number of links in this graph.""" return len(self._linksWithoutID) + len(self._linksWithID) def IterateVertices(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: """ Iterate all or selected vertices of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip vertices in the generator. :param predicate: Filter function accepting any vertex and returning a boolean. :returns: A generator to iterate all vertices. """ if predicate is None: yield from self._verticesWithoutID yield from self._verticesWithID.values() else: for vertex in self._verticesWithoutID: if predicate(vertex): yield vertex for vertex in self._verticesWithID.values(): if predicate(vertex): yield vertex def IterateRoots(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: """ Iterate all or selected roots (vertices without inbound edges / without predecessors) of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip vertices in the generator. :param predicate: Filter function accepting any vertex and returning a boolean. :returns: A generator to iterate all vertices without inbound edges. .. seealso:: :meth:`IterateLeafs` |br| |rarr| Iterate leafs of a graph. :meth:`Vertex.IsRoot ` |br| |rarr| Check if a vertex is a root vertex in the graph. :meth:`Vertex.IsLeaf ` |br| |rarr| Check if a vertex is a leaf vertex in the graph. """ if predicate is None: for vertex in self._verticesWithoutID: if len(vertex._inboundEdges) == 0: yield vertex for vertex in self._verticesWithID.values(): if len(vertex._inboundEdges) == 0: yield vertex else: for vertex in self._verticesWithoutID: if len(vertex._inboundEdges) == 0 and predicate(vertex): yield vertex for vertex in self._verticesWithID.values(): if len(vertex._inboundEdges) == 0 and predicate(vertex): yield vertex def IterateLeafs(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: """ Iterate all or selected leafs (vertices without outbound edges / without successors) of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip vertices in the generator. :param predicate: Filter function accepting any vertex and returning a boolean. :returns: A generator to iterate all vertices without outbound edges. .. seealso:: :meth:`IterateRoots` |br| |rarr| Iterate roots of a graph. :meth:`Vertex.IsRoot ` |br| |rarr| Check if a vertex is a root vertex in the graph. :meth:`Vertex.IsLeaf ` |br| |rarr| Check if a vertex is a leaf vertex in the graph. """ if predicate is None: for vertex in self._verticesWithoutID: if len(vertex._outboundEdges) == 0: yield vertex for vertex in self._verticesWithID.values(): if len(vertex._outboundEdges) == 0: yield vertex else: for vertex in self._verticesWithoutID: if len(vertex._outboundEdges) == 0 and predicate(vertex): yield vertex for vertex in self._verticesWithID.values(): if len(vertex._outboundEdges) == 0 and predicate(vertex): yield vertex # def IterateBFS(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: # raise NotImplementedError() # # def IterateDFS(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: # raise NotImplementedError() def IterateTopologically(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: """ Iterate all or selected vertices in topological order. If parameter ``predicate`` is not None, the given filter function is used to skip vertices in the generator. :param predicate: Filter function accepting any vertex and returning a boolean. :returns: A generator to iterate all vertices in topological order. :except CycleError: Raised if graph is cyclic, thus topological sorting isn't possible. """ outboundEdgeCounts = {} leafVertices = [] for vertex in self._verticesWithoutID: if (count := len(vertex._outboundEdges)) == 0: leafVertices.append(vertex) else: outboundEdgeCounts[vertex] = count for vertex in self._verticesWithID.values(): if (count := len(vertex._outboundEdges)) == 0: leafVertices.append(vertex) else: outboundEdgeCounts[vertex] = count if not leafVertices: raise CycleError(f"Graph has no leafs. Thus, no topological sorting exists.") overallCount = len(outboundEdgeCounts) + len(leafVertices) def removeVertex(vertex: Vertex): nonlocal overallCount overallCount -= 1 for inboundEdge in vertex._inboundEdges: sourceVertex = inboundEdge.Source count = outboundEdgeCounts[sourceVertex] - 1 outboundEdgeCounts[sourceVertex] = count if count == 0: leafVertices.append(sourceVertex) if predicate is None: for vertex in leafVertices: yield vertex removeVertex(vertex) else: for vertex in leafVertices: if predicate(vertex): yield vertex removeVertex(vertex) if overallCount == 0: return elif overallCount > 0: raise CycleError(f"Graph has remaining vertices. Thus, the graph has at least one cycle.") raise InternalError(f"Graph data structure is corrupted.") # pragma: no cover def IterateEdges(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> Generator[Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType], None, None]: """ Iterate all or selected edges of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip edges in the generator. :param predicate: Filter function accepting any edge and returning a boolean. :returns: A generator to iterate all edges. """ if predicate is None: yield from self._edgesWithoutID yield from self._edgesWithID.values() else: for edge in self._edgesWithoutID: if predicate(edge): yield edge for edge in self._edgesWithID.values(): if predicate(edge): yield edge def IterateLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link[LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: """ Iterate all or selected links of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator. :param predicate: Filter function accepting any link and returning a boolean. :returns: A generator to iterate all links. """ if predicate is None: yield from self._linksWithoutID yield from self._linksWithID.values() else: for link in self._linksWithoutID: if predicate(link): yield link for link in self._linksWithID.values(): if predicate(link): yield link def ReverseEdges(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> None: """ Reverse all or selected edges of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip edges. :param predicate: Filter function accepting any edge and returning a boolean. """ if predicate is None: for edge in self._edgesWithoutID: swap = edge._source edge._source = edge._destination edge._destination = swap for edge in self._edgesWithID.values(): swap = edge._source edge._source = edge._destination edge._destination = swap for vertex in self._verticesWithoutID: swap = vertex._inboundEdges vertex._inboundEdges = vertex._outboundEdges vertex._outboundEdges = swap for vertex in self._verticesWithID.values(): swap = vertex._inboundEdges vertex._inboundEdges = vertex._outboundEdges vertex._outboundEdges = swap else: for edge in self._edgesWithoutID: if predicate(edge): edge.Reverse() for edge in self._edgesWithID.values(): if predicate(edge): edge.Reverse() def ReverseLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> None: """ Reverse all or selected links of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip links. :param predicate: Filter function accepting any link and returning a boolean. """ if predicate is None: for link in self._linksWithoutID: swap = link._source link._source = link._destination link._destination = swap for link in self._linksWithID.values(): swap = link._source link._source = link._destination link._destination = swap for vertex in self._verticesWithoutID: swap = vertex._inboundLinks vertex._inboundLinks = vertex._outboundLinks vertex._outboundLinks = swap for vertex in self._verticesWithID.values(): swap = vertex._inboundLinks vertex._inboundLinks = vertex._outboundLinks vertex._outboundLinks = swap else: for link in self._linksWithoutID: if predicate(link): link.Reverse() for link in self._linksWithID.values(): if predicate(link): link.Reverse() def RemoveEdges(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> None: """ Remove all or selected edges of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip edges. :param predicate: Filter function accepting any edge and returning a boolean. """ if predicate is None: for edge in self._edgesWithoutID: edge._Delete() for edge in self._edgesWithID.values(): edge._Delete() self._edgesWithoutID = [] self._edgesWithID = {} for vertex in self._verticesWithoutID: vertex._inboundEdges = [] vertex._outboundEdges = [] for vertex in self._verticesWithID.values(): vertex._inboundEdges = [] vertex._outboundEdges = [] else: delEdges = [edge for edge in self._edgesWithID.values() if predicate(edge)] for edge in delEdges: del self._edgesWithID[edge._id] edge._source._outboundEdges.remove(edge) edge._destination._inboundEdges.remove(edge) edge._Delete() for edge in self._edgesWithoutID: if predicate(edge): self._edgesWithoutID.remove(edge) edge._source._outboundEdges.remove(edge) edge._destination._inboundEdges.remove(edge) edge._Delete() def RemoveLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> None: """ Remove all or selected links of a graph. If parameter ``predicate`` is not None, the given filter function is used to skip links. :param predicate: Filter function accepting any link and returning a boolean. """ if predicate is None: for link in self._linksWithoutID: link._Delete() for link in self._linksWithID.values(): link._Delete() self._linksWithoutID = [] self._linksWithID = {} for vertex in self._verticesWithoutID: vertex._inboundLinks = [] vertex._outboundLinks = [] for vertex in self._verticesWithID.values(): vertex._inboundLinks = [] vertex._outboundLinks = [] else: delLinks = [link for link in self._linksWithID.values() if predicate(link)] for link in delLinks: del self._linksWithID[link._id] link._source._outboundLinks.remove(link) link._destination._inboundLinks.remove(link) link._Delete() for link in self._linksWithoutID: if predicate(link): self._linksWithoutID.remove(link) link._source._outboundLinks.remove(link) link._destination._inboundLinks.remove(link) link._Delete() def HasCycle(self) -> bool: """ .. todo:: GRAPH::BaseGraph::HasCycle Needs documentation. """ # IsAcyclic ? # Handle trivial case if graph is empty if len(self._verticesWithID) + len(self._verticesWithoutID) == 0: return False outboundEdgeCounts = {} leafVertices = [] for vertex in self._verticesWithoutID: if (count := len(vertex._outboundEdges)) == 0: leafVertices.append(vertex) else: outboundEdgeCounts[vertex] = count for vertex in self._verticesWithID.values(): if (count := len(vertex._outboundEdges)) == 0: leafVertices.append(vertex) else: outboundEdgeCounts[vertex] = count # If there are no leafs, then each vertex has at least one inbound and one outbound edges. Thus, there is a cycle. if not leafVertices: return True overallCount = len(outboundEdgeCounts) + len(leafVertices) for vertex in leafVertices: overallCount -= 1 for inboundEdge in vertex._inboundEdges: sourceVertex = inboundEdge.Source count = outboundEdgeCounts[sourceVertex] - 1 outboundEdgeCounts[sourceVertex] = count if count == 0: leafVertices.append(sourceVertex) # If all vertices were processed, no cycle exists. if overallCount == 0: return False # If there are remaining vertices, then a cycle exists. elif overallCount > 0: return True raise InternalError(f"Graph data structure is corrupted.") # pragma: no cover @export class Subgraph( BaseGraph[ SubgraphDictKeyType, SubgraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ], Generic[ SubgraphDictKeyType, SubgraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ] ): """ .. todo:: GRAPH::Subgraph Needs documentation. """ _graph: 'Graph' def __init__( self, graph: 'Graph', name: Nullable[str] = None, # vertices: Nullable[Iterable[Vertex]] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::Subgraph::init Needs documentation. :param graph: The reference to the graph. :param name: The optional name of the new sub-graph. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ if graph is None: raise ValueError("Parameter 'graph' is None.") if not isinstance(graph, Graph): ex = TypeError("Parameter 'graph' is not of type 'Graph'.") ex.add_note(f"Got type '{getFullyQualifiedName(graph)}'.") raise ex super().__init__(name, keyValuePairs) graph._subgraphs.add(self) self._graph = graph def __del__(self) -> None: """ .. todo:: GRAPH::Subgraph::del Needs documentation. """ super().__del__() @readonly def Graph(self) -> 'Graph': """ Read-only property to access the graph, this subgraph is associated to (:attr:`_graph`). :returns: The graph this subgraph is associated to. """ return self._graph def __str__(self) -> str: """ .. todo:: GRAPH::Subgraph::str Needs documentation. """ return self._name if self._name is not None else "Unnamed subgraph" @export class View( BaseWithVertices[ ViewDictKeyType, ViewDictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ], Generic[ ViewDictKeyType, ViewDictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ] ): """ .. todo:: GRAPH::View Needs documentation. """ def __init__( self, graph: 'Graph', name: Nullable[str] = None, vertices: Nullable[Iterable[Vertex]] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::View::init Needs documentation. :param graph: The reference to the graph. :param name: The optional name of the new view. :param vertices: The optional list of vertices in the new view. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ super().__init__(graph, name, vertices, keyValuePairs) graph._views.add(self) def __del__(self) -> None: """ .. todo:: GRAPH::View::del Needs documentation. """ super().__del__() def __str__(self) -> str: """ .. todo:: GRAPH::View::str Needs documentation. """ return self._name if self._name is not None else "Unnamed view" @export class Component( BaseWithVertices[ ComponentDictKeyType, ComponentDictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ], Generic[ ComponentDictKeyType, ComponentDictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ] ): """ .. todo:: GRAPH::Component Needs documentation. """ def __init__( self, graph: 'Graph', name: Nullable[str] = None, vertices: Nullable[Iterable[Vertex]] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::Component::init Needs documentation. :param graph: The reference to the graph. :param name: The optional name of the new component. :param vertices: The optional list of vertices in the new component. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. """ super().__init__(graph, name, vertices, keyValuePairs) graph._components.add(self) def __del__(self) -> None: """ .. todo:: GRAPH::Component::del Needs documentation. """ super().__del__() def __str__(self) -> str: """ .. todo:: GRAPH::Component::str Needs documentation. """ return self._name if self._name is not None else "Unnamed component" @export class Graph( BaseGraph[ GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ], Generic[ GraphDictKeyType, GraphDictValueType, ComponentDictKeyType, ComponentDictValueType, SubgraphDictKeyType, SubgraphDictValueType, ViewDictKeyType, ViewDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType ] ): """ A **graph** data structure is represented by an instance of :class:`~pyTooling.Graph.Graph` holding references to all nodes. Nodes are instances of :class:`~pyTooling.Graph.Vertex` classes and directed links between nodes are made of :class:`~pyTooling.Graph.Edge` instances. A graph can have attached meta information as key-value-pairs. """ _subgraphs: Set[Subgraph[SubgraphDictKeyType, SubgraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] _views: Set[View[ViewDictKeyType, ViewDictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] _components: Set[Component[ComponentDictKeyType, ComponentDictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] def __init__( self, name: Nullable[str] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None ) -> None: """ .. todo:: GRAPH::Graph::init Needs documentation. :param name: The optional name of the new graph. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs.# """ super().__init__(name, keyValuePairs) self._subgraphs = set() self._views = set() self._components = set() def __del__(self) -> None: """ .. todo:: GRAPH::Graph::del Needs documentation. """ try: del self._subgraphs del self._views del self._components except AttributeError: pass super().__del__() @readonly def Subgraphs(self) -> Set[Subgraph]: """Read-only property to access the subgraphs in this graph (:attr:`_subgraphs`). :returns: The set of subgraphs in this graph.""" return self._subgraphs @readonly def Views(self) -> Set[View]: """Read-only property to access the views in this graph (:attr:`_views`). :returns: The set of views in this graph.""" return self._views @readonly def Components(self) -> Set[Component]: """Read-only property to access the components in this graph (:attr:`_components`). :returns: The set of components in this graph.""" return self._components @readonly def SubgraphCount(self) -> int: """Read-only property to access the number of subgraphs in this graph. :returns: The number of subgraphs in this graph.""" return len(self._subgraphs) @readonly def ViewCount(self) -> int: """Read-only property to access the number of views in this graph. :returns: The number of views in this graph.""" return len(self._views) @readonly def ComponentCount(self) -> int: """Read-only property to access the number of components in this graph. :returns: The number of components in this graph.""" return len(self._components) def __iter__(self) -> typing_Iterator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]]: """ .. todo:: GRAPH::Graph::iter Needs documentation. """ def gen(): yield from self._verticesWithoutID yield from self._verticesWithID return iter(gen()) def HasVertexByID(self, vertexID: Nullable[VertexIDType]) -> bool: """ .. todo:: GRAPH::Graph::HasVertexByID Needs documentation. """ if vertexID is None: return len(self._verticesWithoutID) >= 1 else: return vertexID in self._verticesWithID def HasVertexByValue(self, value: Nullable[VertexValueType]) -> bool: """ .. todo:: GRAPH::Graph::HasVertexByValue Needs documentation. """ return any(vertex._value == value for vertex in chain(self._verticesWithoutID, self._verticesWithID.values())) def GetVertexByID(self, vertexID: Nullable[VertexIDType]) -> Vertex: """ .. todo:: GRAPH::Graph::GetVertexByID Needs documentation. """ if vertexID is None: if (l := len(self._verticesWithoutID)) == 1: return self._verticesWithoutID[0] elif l == 0: raise KeyError(f"Found no vertex with ID `None`.") else: raise KeyError(f"Found multiple vertices with ID `None`.") else: return self._verticesWithID[vertexID] def GetVertexByValue(self, value: Nullable[VertexValueType]) -> Vertex: """ .. todo:: GRAPH::Graph::GetVertexByValue Needs documentation. """ # FIXME: optimize: iterate only until first item is found and check for a second to produce error vertices = [vertex for vertex in chain(self._verticesWithoutID, self._verticesWithID.values()) if vertex._value == value] if (l := len(vertices)) == 1: return vertices[0] elif l == 0: raise KeyError(f"Found no vertex with Value == `{value}`.") else: raise KeyError(f"Found multiple vertices with Value == `{value}`.") def CopyGraph(self) -> 'Graph': raise NotImplementedError() def CopyVertices(self, predicate: Nullable[Callable[[Vertex], bool]] = None, copyGraphDict: bool = True, copyVertexDict: bool = True) -> 'Graph': """ Create a new graph and copy all or selected vertices of the original graph. If parameter ``predicate`` is not None, the given filter function is used to skip vertices. :param predicate: Filter function accepting any vertex and returning a boolean. :param copyGraphDict: If ``True``, copy all graph attached attributes into the new graph. :param copyVertexDict: If ``True``, copy all vertex attached attributes into the new vertices. """ graph = Graph(self._name) if copyGraphDict: graph._dict = self._dict.copy() if predicate is None: for vertex in self._verticesWithoutID: v = Vertex(None, vertex._value, graph=graph) if copyVertexDict: v._dict = vertex._dict.copy() for vertexID, vertex in self._verticesWithID.items(): v = Vertex(vertexID, vertex._value, graph=graph) if copyVertexDict: v._dict = vertex._dict.copy() else: for vertex in self._verticesWithoutID: if predicate(vertex): v = Vertex(None, vertex._value, graph=graph) if copyVertexDict: v._dict = vertex._dict.copy() for vertexID, vertex in self._verticesWithID.items(): if predicate(vertex): v = Vertex(vertexID, vertex._value, graph=graph) if copyVertexDict: v._dict = vertex._dict.copy() return graph # class Iterator(): # visited = [False for _ in range(self.__len__())] # def CheckForNegativeCycles(self): # raise NotImplementedError() # # Bellman-Ford # # Floyd-Warshall # # def IsStronglyConnected(self): # raise NotImplementedError() # # def GetStronglyConnectedComponents(self): # raise NotImplementedError() # # Tarjan's and Kosaraju's algorithm # # def TravelingSalesmanProblem(self): # raise NotImplementedError() # # Held-Karp # # branch and bound # # def GetBridges(self): # raise NotImplementedError() # # def GetArticulationPoints(self): # raise NotImplementedError() # # def MinimumSpanningTree(self): # raise NotImplementedError() # # Kruskal # # Prim's algorithm # # Buruvka's algorithm def __repr__(self) -> str: """ .. todo:: GRAPH::Graph::repr Needs documentation. """ statistics = f", vertices: {self.VertexCount}, edges: {self.EdgeCount}" if self._name is None: return f"" else: return f"" def __str__(self) -> str: """ .. todo:: GRAPH::Graph::str Needs documentation. """ if self._name is None: return f"Graph: unnamed graph" else: return f"Graph: '{self._name}'" pyTooling-8.11.0/pyTooling/Licensing/000077500000000000000000000000001513317154500174715ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Licensing/__init__.py000066400000000000000000000244401513317154500216060ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_) ___ ___ _ __ ___(_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ __/ _ \ '_ \/ __| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | (_| __/ | | \__ \ | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|\___\___|_| |_|___/_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ The Licensing module implements mapping tables for various license names and identifiers. .. seealso:: List of SPDX identifiers: * https://spdx.org/licenses/ * https://github.com/spdx/license-list-XML List of `Python classifiers `__ .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from dataclasses import dataclass from typing import Any, Dict, Optional as Nullable try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Licensing] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Licensing] Could not import directly!") raise ex __all__ = [ "PYTHON_LICENSE_NAMES", "Apache_2_0_License", "BSD_3_Clause_License", "GPL_2_0_or_later", "MIT_License", "SPDX_INDEX" ] @export @dataclass class PythonLicenseName: """A *data class* to represent the license's short name and the package classifier for a license.""" ShortName: str #: License's short name Classifier: str #: Package classifier for a license. def __str__(self) -> str: """ The string representation of this name tuple returns the short name of the license. :returns: Short name of the license. """ return self.ShortName #: Mapping of SPDX identifiers to Python license names PYTHON_LICENSE_NAMES: Dict[str, PythonLicenseName] = { "Apache-2.0": PythonLicenseName("Apache 2.0", "Apache Software License"), "BSD-3-Clause": PythonLicenseName("BSD", "BSD License"), "MIT": PythonLicenseName("MIT", "MIT License"), "GPL-2.0-or-later": PythonLicenseName("GPL-2.0-or-later", "GNU General Public License v2 or later (GPLv2+)"), } @export class License(metaclass=ExtendedType, slots=True): """Representation of a license.""" _spdxIdentifier: str #: Unique SPDX identifier. _name: str #: Name of the license. _osiApproved: bool #: OSI approval status _fsfApproved: bool #: FSF approval status def __init__(self, spdxIdentifier: str, name: str, osiApproved: bool = False, fsfApproved: bool = False) -> None: self._spdxIdentifier = spdxIdentifier self._name = name self._osiApproved = osiApproved self._fsfApproved = fsfApproved @readonly def Name(self) -> str: """ Returns the license' name. :returns: License name. """ return self._name @readonly def SPDXIdentifier(self) -> str: """ Returns the license' unique `SPDX identifier `__. :returns: The the unique SPDX identifier. """ return self._spdxIdentifier @readonly def OSIApproved(self) -> bool: """ Returns true, if the license is approved by OSI (`Open Source Initiative `__). :returns: ``True``, if the license is approved by the Open Source Initiative. """ return self._osiApproved @readonly def FSFApproved(self) -> bool: """ Returns true, if the license is approved by FSF (`Free Software Foundation `__). :returns: ``True``, if the license is approved by the Free Software Foundation. """ return self._fsfApproved @readonly def PythonLicenseName(self) -> str: """ Returns the Python license name for this license if it's defined. :returns: The Python license name. :raises ValueError: If there is no license name defined for the license. |br| (See and check :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES`) """ try: item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] except KeyError as ex: raise ValueError("License has no Python specify information.") from ex return item.ShortName @readonly def PythonClassifier(self) -> str: """ Returns the Python package classifier for this license if it's defined. :returns: The Python package classifier. :raises ValueError: If there is no classifier defined for the license. |br| (See and check :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES`) .. seealso:: List of `Python classifiers `__ """ try: item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] except KeyError as ex: raise ValueError(f"License has no Python specify information.") from ex osi = "OSI Approved :: " if self._osiApproved else "" return f"License :: {osi}{item.Classifier}" def __eq__(self, other: Any) -> bool: """ Returns true, if both licenses are identical (comparison based on SPDX identifiers). :returns: ``True``, if both licenses are identical. :raises TypeError: If second operand is not of type :class:`License` or :class:`str`. """ if isinstance(other, License): return self._spdxIdentifier == other._spdxIdentifier else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by equal operator.") ex.add_note(f"Supported types for second operand: License, str") raise ex def __ne__(self, other: Any) -> bool: """ Returns true, if both licenses are not identical (comparison based on SPDX identifiers). :returns: ``True``, if both licenses are not identical. :raises TypeError: If second operand is not of type :class:`License` or :class:`str`. """ if isinstance(other, License): return self._spdxIdentifier != other._spdxIdentifier else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by unequal operator.") ex.add_note(f"Supported types for second operand: License, str") raise ex def __le__(self, other: Any) -> bool: """Returns true, if both licenses are compatible.""" raise NotImplementedError("License compatibility check is not yet implemented.") def __ge__(self, other: Any) -> bool: """Returns true, if both licenses are compatible.""" raise NotImplementedError("License compatibility check is not yet implemented.") def __repr__(self) -> str: """ Returns the internal unique representation (a.k.a SPDX identifier). :returns: SPDX identifier of the license. """ return self._spdxIdentifier def __str__(self) -> str: """ Returns the license' name. :returns: Name of the license. """ return self._name Apache_2_0_License = License("Apache-2.0", "Apache License 2.0", True, True) BSD_3_Clause_License = License("BSD-3-Clause", "BSD 3-Clause Revised License", True, True) GPL_2_0_or_later = License("GPL-2.0-or-later", "GNU General Public License v2.0 or later", True, True) MIT_License = License("MIT", "MIT License", True, True) #: Mapping of predefined licenses SPDX_INDEX: Dict[str, License] = { "Apache-2.0": Apache_2_0_License, "BSD-3-Clause": BSD_3_Clause_License, "GPL-2.0-or-later": GPL_2_0_or_later, "MIT": MIT_License } pyTooling-8.11.0/pyTooling/LinkedList/000077500000000000000000000000001513317154500176205ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/LinkedList/__init__.py000066400000000000000000000673101513317154500217400ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_)_ __ | | _____ __| | | (_)___| |_ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | | '_ \| |/ / _ \/ _` | | | / __| __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | | | | < __/ (_| | |___| \__ \ |_ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|_| |_|_|\_\___|\__,_|_____|_|___/\__| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """An object-oriented doubly linked-list data structure for Python.""" from collections.abc import Sized from typing import Generic, TypeVar, Optional as Nullable, Callable, Iterable, Generator, Tuple, List, Any try: from pyTooling.Decorators import readonly, export from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType from pyTooling.Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.LinkedList] Could not import from 'pyTooling.*'!") try: from Decorators import readonly, export from Exceptions import ToolingException from MetaClasses import ExtendedType from Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.LinkedList] Could not import directly!") raise ex _NodeKey = TypeVar("_NodeKey") _NodeValue = TypeVar("_NodeValue") @export class LinkedListException(ToolingException): """Base-exception of all exceptions raised by :mod:`pyTooling.LinkedList`.""" @export class Node(Generic[_NodeKey, _NodeValue], metaclass=ExtendedType, slots=True): """ The node in an object-oriented doubly linked-list. It contains a reference to the doubly linked list (:attr:`_list`), the previous node (:attr:`_previous`), the next node (:attr:`_next`) and the data (:attr:`_value`). Optionally, a key (:attr:`_key`) can be stored for sorting purposes. The :attr:`_previous` field of the **first node** in a doubly linked list is ``None``. Similarly, the :attr:`_next` field of the **last node** is ``None``. ``None`` represents the end of the linked list when iterating it node-by-node. """ _linkedList: Nullable["LinkedList[_NodeValue]"] #: Reference to the doubly linked list instance. _previousNode: Nullable["Node[_NodeKey, _NodeValue]"] #: Reference to the previous node. _nextNode: Nullable["Node[_NodeKey, _NodeValue]"] #: Reference to the next node. _key: Nullable[_NodeKey] #: The sortable key of the node. _value: _NodeValue #: The value of the node. def __init__( self, value: _NodeValue, key: Nullable[_NodeKey] = None, previousNode: Nullable["Node[_NodeKey, _NodeValue]"] = None, nextNode: Nullable["Node[_NodeKey, _NodeValue]"] = None ) -> None: """ Initialize a linked list node. :param value: Value to store in the node. :param key: Optional sortable key to store in the node. :param previousNode: Optional reference to the previous node. :param nextNode: Optional reference to the next node. :raises TypeError: If parameter 'previous' is not of type :class:`Node`. :raises TypeError: If parameter 'next' is not of type :class:`Node`. """ self._previousNode = previousNode self._nextNode = nextNode self._value = value self._key = value # Attache to previous node if previousNode is not None: if not isinstance(previousNode, Node): ex = TypeError(f"Parameter 'previous' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(previousNode)}'.") raise ex # PreviousNode is part of a list if previousNode._linkedList is not None: self._linkedList = previousNode._linkedList self._linkedList._count += 1 # Check if previous was the last node if previousNode._nextNode is None: self._nextNode = None self._linkedList._lastNode = self else: self._nextNode = previousNode._nextNode self._nextNode._previousNode = self else: self._linkedList = None previousNode._nextNode = self if nextNode is not None: if not isinstance(nextNode, Node): ex = TypeError(f"Parameter 'next' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(nextNode)}'.") raise ex if nextNode._linkedList is not None: if self._linkedList is not None: if self._linkedList is not previousNode._linkedList: raise ValueError() previousNode._nextNode = self elif nextNode is not None: if not isinstance(nextNode, Node): ex = TypeError(f"Parameter 'next' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(nextNode)}'.") raise ex # NextNode is part of a list if nextNode._linkedList is not None: self._linkedList = nextNode._linkedList self._linkedList._count += 1 # Check if next was the first node if nextNode._previousNode is None: self._previousNode = None self._linkedList._firstNode = self else: self._previousNode = nextNode._previousNode self._previousNode._nextNode = self else: self._linkedList = None nextNode._previousNode = self else: self._linkedList = None @readonly def List(self) -> Nullable["LinkedList[_NodeValue]"]: """ Read-only property to access the linked list, this node belongs to. :return: The linked list, this node is part of, or ``None``. """ return self._linkedList @readonly def PreviousNode(self) -> Nullable["Node[_NodeKey, _NodeValue]"]: """ Read-only property to access node's predecessor. This reference is ``None`` if the node is the first node in the doubly linked list. :return: The node before the current node or ``None``. """ return self._previousNode @readonly def NextNode(self) -> Nullable["Node[_NodeKey, _NodeValue]"]: """ Read-only property to access node's successor. This reference is ``None`` if the node is the last node in the doubly linked list. :return: The node after the current node or ``None``. """ return self._nextNode @property def Key(self) -> _NodeKey: """ Property to access the node's internal key. The key can be a scalar or a reference to an object. :return: The node's key. """ return self._key @Key.setter def Key(self, key: _NodeKey) -> None: self._key = key @property def Value(self) -> _NodeValue: """ Property to access the node's internal data. The data can be a scalar or a reference to an object. :return: The node's value. """ return self._value @Value.setter def Value(self, value: _NodeValue) -> None: self._value = value def InsertNodeBefore(self, node: "Node[_NodeKey, _NodeValue]") -> None: """ Insert a node before this node. :param node: Node to insert. :raises ValueError: If parameter 'node' is ``None``. :raises TypeError: If parameter 'node' is not of type :class:`Node`. :raises LinkedListException: If parameter 'node' is already part of another linked list. """ if node is None: raise ValueError(f"Parameter 'node' is None.") if not isinstance(node, Node): ex = TypeError(f"Parameter 'node' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(next)}'.") raise ex if node._linkedList is not None: raise LinkedListException(f"Parameter 'node' belongs to another linked list.") node._linkedList = self._linkedList node._nextNode = self node._previousNode = self._previousNode if self._previousNode is None: self._linkedList._firstNode = node else: self._previousNode._nextNode = node self._previousNode = node self._linkedList._count += 1 def InsertNodeAfter(self, node: "Node[_NodeKey, _NodeValue]") -> None: """ Insert a node after this node. :param node: Node to insert. :raises ValueError: If parameter 'node' is ``None``. :raises TypeError: If parameter 'node' is not of type :class:`Node`. :raises LinkedListException: If parameter 'node' is already part of another linked list. """ if node is None: raise ValueError(f"Parameter 'node' is None.") if not isinstance(node, Node): ex = TypeError(f"Parameter 'node' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(next)}'.") raise ex if node._linkedList is not None: raise LinkedListException(f"Parameter 'node' belongs to another linked list.") node._linkedList = self._linkedList node._previousNode = self node._nextNode = self._nextNode if self._nextNode is None: self._linkedList._lastNode = node else: self._nextNode._previousNode = node self._nextNode = node self._linkedList._count += 1 # move forward # move backward # move by relative pos # move to position # move to begin # move to end # insert tuple/list/linkedlist before # insert tuple/list/linkedlist after # iterate forward for n # iterate backward for n # slice to tuple / list starting from that node # swap left by n # swap right by n def Remove(self) -> _NodeValue: """ Remove this node from the linked list. """ if self._previousNode is None: if self._linkedList is not None: self._linkedList._firstNode = self._nextNode self._linkedList._count -= 1 if self._nextNode is None: self._linkedList._lastNode = None self._linkedList = None if self._nextNode is not None: self._nextNode._previousNode = None self._nextNode = None elif self._nextNode is None: if self._linkedList is not None: self._linkedList._lastNode = self._previousNode self._linkedList._count -= 1 self._linkedList = None self._previousNode._nextNode = None self._previousNode = None else: self._previousNode._nextNode = self._nextNode self._nextNode._previousNode = self._previousNode self._nextNode = None self._previousNode = None if self._linkedList is not None: self._linkedList._count -= 1 self._linkedList = None return self._value def IterateToFirst(self, includeSelf: bool = False) -> Generator["Node[_NodeKey, _NodeValue]", None, None]: """ Return a generator iterating backward from this node to the list's first node. Optionally, this node can be included into the generated sequence. :param includeSelf: If ``True``, include this node into the sequence, otherwise start at previous node. :return: A sequence of nodes towards the list's first node. """ previousNode = self._previousNode if includeSelf: yield self node = previousNode while node is not None: previousNode = node._previousNode yield node node = previousNode def IterateToLast(self, includeSelf: bool = False) -> Generator["Node[_NodeKey, _NodeValue]", None, None]: """ Return a generator iterating forward from this node to the list's last node. Optionally, this node can be included into the generated sequence by setting. :param includeSelf: If ``True``, include this node into the sequence, otherwise start at next node. :return: A sequence of nodes towards the list's last node. """ nextNode = self._nextNode if includeSelf: yield self node = nextNode while node is not None: nextNode = node._nextNode yield node node = nextNode def __repr__(self) -> str: return f"Node: {self._value}" @export class LinkedList(Generic[_NodeKey, _NodeValue], metaclass=ExtendedType, slots=True): """An object-oriented doubly linked-list.""" _firstNode: Nullable[Node[_NodeKey, _NodeValue]] #: Reference to the first node of the linked list. _lastNode: Nullable[Node[_NodeKey, _NodeValue]] #: Reference to the last node of the linked list. _count: int #: Number of nodes in the linked list. # allow iterable to initialize the list def __init__(self, nodes: Nullable[Iterable[Node[_NodeKey, _NodeValue]]] = None) -> None: """ Initialize an empty linked list. Optionally, an iterable can be given to initialize the linked list. The order is preserved. :param nodes: Optional iterable to initialize the linked list. :raises TypeError: If parameter 'nodes' is not an :class:`iterable `. :raises TypeError: If parameter 'nodes' items are not of type :class:`Node`. :raises LinkedListException: If parameter 'nodes' contains items which are already part of another linked list. """ if nodes is None: self._firstNode = None self._lastNode = None self._count = 0 elif not isinstance(nodes, Iterable): ex = TypeError(f"Parameter 'nodes' is not an iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(next)}'.") raise ex else: if isinstance(nodes, Sized) and len(nodes) == 0: self._firstNode = None self._lastNode = None self._count = 0 return try: first = next(iterator := iter(nodes)) except StopIteration: self._firstNode = None self._lastNode = None self._count = 0 return if not isinstance(first, Node): ex = TypeError(f"First element in parameter 'nodes' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(first)}'.") raise ex elif first._linkedList is not None: raise LinkedListException(f"First element in parameter 'nodes' is assigned to different list.") position = 1 first._linkedList = self first._previousNode = None self._firstNode = previous = node = first for node in iterator: if not isinstance(node, Node): ex = TypeError(f"{position}. element in parameter 'nodes' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(node)}'.") raise ex elif node._linkedList is not None: raise LinkedListException(f"{position}. element in parameter 'nodes' is assigned to different list.") node._linkedList = self node._previousNode = previous previous._nextNode = node previous = node position += 1 self._lastNode = node self._count = position node._nextNode = None @readonly def IsEmpty(self) -> int: """ Read-only property to access the number of . This reference is ``None`` if the node is the last node in the doubly linked list. :return: ``True`` if linked list is empty, otherwise ``False`` """ return self._count == 0 @readonly def Count(self) -> int: """ Read-only property to access the number of nodes in the linked list. :return: Number of nodes. """ return self._count @readonly def FirstNode(self) -> Nullable[Node[_NodeKey, _NodeValue]]: """ Read-only property to access the first node in the linked list. In case the list is empty, ``None`` is returned. :return: First node. """ return self._firstNode @readonly def LastNode(self) -> Nullable[Node[_NodeKey, _NodeValue]]: """ Read-only property to access the last node in the linked list. In case the list is empty, ``None`` is returned. :return: Last node. """ return self._lastNode def Clear(self) -> None: """ Clear the linked list. """ self._firstNode = None self._lastNode = None self._count = 0 def InsertBeforeFirst(self, node: Node[_NodeKey, _NodeValue]) -> None: """ Insert a node before the first node. :param node: Node to insert. :raises ValueError: If parameter 'node' is ``None``. :raises TypeError: If parameter 'node' is not of type :class:`Node`. :raises LinkedListException: If parameter 'node' is already part of another linked list. """ if node is None: raise ValueError(f"Parameter 'node' is None.") if not isinstance(node, Node): ex = TypeError(f"Parameter 'node' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(next)}'.") raise ex if node._linkedList is not None: raise LinkedListException(f"Parameter 'node' belongs to another linked list.") node._linkedList = self node._previousNode = None node._nextNode = self._firstNode if self._firstNode is None: self._lastNode = node else: self._firstNode._previousNode = node self._firstNode = node self._count += 1 def InsertAfterLast(self, node: Node[_NodeKey, _NodeValue]) -> None: """ Insert a node after the last node. :param node: Node to insert. :raises ValueError: If parameter 'node' is ``None``. :raises TypeError: If parameter 'node' is not of type :class:`Node`. :raises LinkedListException: If parameter 'node' is already part of another linked list. """ if node is None: raise ValueError(f"Parameter 'node' is None.") if not isinstance(node, Node): ex = TypeError(f"Parameter 'node' is not of type Node.") ex.add_note(f"Got type '{getFullyQualifiedName(next)}'.") raise ex if node._linkedList is not None: raise LinkedListException(f"Parameter 'node' belongs to another linked list.") node._linkedList = self node._nextNode = None node._previousNode = self._lastNode if self._lastNode is None: self._firstNode = node else: node._previousNode._nextNode = node self._lastNode = node self._count += 1 def RemoveFirst(self) -> Node[_NodeKey, _NodeValue]: """ Remove first node from linked list. :return: First node. :raises LinkedListException: If linked list is empty. """ if self._firstNode is None: raise LinkedListException(f"Linked list is empty.") node = self._firstNode self._firstNode = node._nextNode if self._firstNode is None: self._lastNode = None self._count = 0 else: self._firstNode._previousNode = None self._count -= 1 node._linkedList = None node._nextNode = None return node def RemoveLast(self) -> Node[_NodeKey, _NodeValue]: """ Remove last node from linked list. :return: Last node. :raises LinkedListException: If linked list is empty. """ if self._lastNode is None: raise LinkedListException(f"Linked list is empty.") node = self._lastNode self._lastNode = node._previousNode if self._lastNode is None: self._firstNode = None self._count = 0 else: self._lastNode._nextNode = None self._count -= 1 node._linkedList = None node._previousNode = None return node def GetNodeByIndex(self, index: int) -> Node[_NodeKey, _NodeValue]: """ Access a node in the linked list by position. :param index: Node position to access. :return: Node at the given position. :raises ValueError: If parameter 'position' is out of range. .. note:: The algorithm starts iterating nodes from the shorter end. """ if index == 0: if self._firstNode is None: ex = ValueError("Parameter 'position' is out of range.") ex.add_note(f"Linked list is empty.") raise ex return self._firstNode elif index == self._count - 1: return self._lastNode elif index >= self._count: ex = ValueError("Parameter 'position' is out of range.") ex.add_note(f"Linked list has {self._count} elements. Requested index: {index}.") raise ex if index < self._count / 2: pos = 1 node = self._firstNode._nextNode while node is not None: if pos == index: return node node = node._nextNode pos += 1 else: # pragma: no cover raise LinkedListException(f"Node position not found.") else: pos = self._count - 2 node = self._lastNode._previousNode while node is not None: if pos == index: return node node = node._previousNode pos -= 1 else: # pragma: no cover raise LinkedListException(f"Node position not found.") def Search(self, predicate: Callable[[Node], bool], reverse: bool = False) -> Node[_NodeKey, _NodeValue]: if self._firstNode is None: raise LinkedListException(f"Linked list is empty.") if not reverse: node = self._firstNode while node is not None: if predicate(node): break node = node._nextNode else: raise LinkedListException(f"Node not found.") else: node = self._lastNode while node is not None: if predicate(node): break node = node._previousNode else: raise LinkedListException(f"Node not found.") return node def Reverse(self) -> None: """ Reverse the order of nodes in the linked list. """ if self._firstNode is None or self._firstNode is self._lastNode: return node = self._lastNode = self._firstNode while node is not None: last = node node = last._nextNode last._nextNode = last._previousNode last._previousNode = node self._firstNode = last def Sort(self, key: Nullable[Callable[[Node[_NodeKey, _NodeValue]], Any]] = None, reverse: bool = False) -> None: """ Sort the linked list in ascending or descending order. The sort operation is **stable**. :param key: Optional function to access a user-defined key for sorting. :param reverse: Optional parameter, if ``True`` sort in descending order, otherwise in ascending order. .. note:: The linked list is converted to an array, which is sorted by quicksort using the builtin :meth:`~list.sort`. Afterward, the sorted array is used to reconstruct the linked list in requested order. """ if (self._firstNode is None) or (self._firstNode is self._lastNode): return if key is None: key = lambda node: node._value sequence = [n for n in self.IterateFromFirst()] sequence.sort(key=key, reverse=reverse) first = sequence[0] position = 1 first._previousNode = None self._firstNode = previous = node = first for node in sequence[1:]: node._previousNode = previous previous._nextNode = node previous = node position += 1 self._lastNode = node self._count = position node._nextNode = None def IterateFromFirst(self) -> Generator[Node[_NodeKey, _NodeValue], None, None]: """ Return a generator iterating forward from list's first node to list's last node. :return: A sequence of nodes towards the list's last node. """ if self._firstNode is None: return node = self._firstNode while node is not None: nextNode = node._nextNode yield node node = nextNode def IterateFromLast(self) -> Generator[Node[_NodeKey, _NodeValue], None, None]: """ Return a generator iterating backward from list's last node to list's first node. :return: A sequence of nodes towards the list's first node. """ if self._lastNode is None: return node = self._lastNode while node is not None: previousNode = node._previousNode yield node node = previousNode def ToList(self, reverse: bool = False) -> List[Node[_NodeKey, _NodeValue]]: """ Convert the linked list to a :class:`list`. Optionally, the resulting list can be constructed in reverse order. :param reverse: Optional parameter, if ``True`` return in reversed order, otherwise in normal order. :return: A list (array) of this linked list's values. """ if self._count == 0: return [] elif reverse: return [n._value for n in self.IterateFromLast()] else: return [n._value for n in self.IterateFromFirst()] def ToTuple(self, reverse: bool = False) -> Tuple[Node[_NodeKey, _NodeValue], ...]: """ Convert the linked list to a :class:`tuple`. Optionally, the resulting tuple can be constructed in reverse order. :param reverse: Optional parameter, if ``True`` return in reversed order, otherwise in normal order. :return: A tuple of this linked list's values. """ if self._count == 0: return tuple() elif reverse: return tuple(n._value for n in self.IterateFromLast()) else: return tuple(n._value for n in self.IterateFromFirst()) # Copy # Sort # merge lists # append / prepend lists # split list # Remove at position (= __delitem__) # Remove by predicate (n times) # Insert at position (= __setitem__) # insert tuple/list/linkedlist at begin # insert tuple/list/linkedlist at end # Find by position (= __getitem__) # Find by predicate from left (n times) # Find by predicate from right (n times) # Count by predicate # slice by start, length from right -> new list # slice by start, length from left # Slice by predicate # iterate start, length from right # iterate start, length from left # iterate by predicate def __len__(self) -> int: """ Returns the number of nodes in the linked list. :returns: Number of nodes. """ return self._count def __getitem__(self, index: int) -> _NodeValue: """ Access a node's value by its index. :param index: Node index to access. :return: Node's value at the given index. :raises ValueError: If parameter 'index' is out of range. .. note:: The algorithm starts iterating nodes from the shorter end. """ return self.GetNodeByIndex(index)._value def __setitem__(self, index: int, value: _NodeValue) -> None: """ Set the value of node at the given position. :param index: Index of the node to modify. :param value: New value for the node's value addressed by index. """ self.GetNodeByIndex(index)._value = value def __delitem__(self, index: int) -> Node[_NodeKey, _NodeValue]: """ Remove a node at the given index. :param index: Index of the node to remove. :return: Removed node. """ node = self.GetNodeByIndex(index) node.Remove() return node._value pyTooling-8.11.0/pyTooling/MetaClasses/000077500000000000000000000000001513317154500177625ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/MetaClasses/__init__.py000066400000000000000000001240471513317154500221030ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # Sven Köhler # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ The MetaClasses package implements Python meta-classes (classes to construct other classes in Python). .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from functools import wraps from itertools import chain from sys import version_info from threading import Condition from types import FunctionType, MethodType from typing import Any, Tuple, List, Dict, Callable, Generator, Set, Iterator, Iterable, Union, NoReturn, Self from typing import Type, TypeVar, Generic, _GenericAlias, ClassVar, Optional as Nullable try: from pyTooling.Exceptions import ToolingException from pyTooling.Decorators import export, readonly except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.MetaClasses] Could not import from 'pyTooling.*'!") try: from Exceptions import ToolingException from Decorators import export, readonly except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.MetaClasses] Could not import directly!") raise ex __all__ = ["M"] TAttr = TypeVar("TAttr") # , bound='Attribute') """A type variable for :class:`~pyTooling.Attributes.Attribute`.""" TAttributeFilter = Union[TAttr, Iterable[TAttr], None] """A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an iterable of those.""" @export class ExtendedTypeError(ToolingException): """The exception is raised by the meta-class :class:`~pyTooling.Metaclasses.ExtendedType`.""" @export class BaseClassWithoutSlotsError(ExtendedTypeError): """ This exception is raised when a class using ``__slots__`` inherits from at-least one base-class not using ``__slots__``. .. seealso:: * :ref:`Python data model for slots ` * :term:`Glossary entry __slots__ <__slots__>` """ @export class BaseClassWithNonEmptySlotsError(ExtendedTypeError): """ This exception is raised when a mixin-class uses slots, but Python prohibits slots. .. important:: To fulfill Python's requirements on slots, pyTooling uses slots only on the prinmary inheritance line. Mixin-classes collect slots, which get materialized when the mixin-class (secondary inheritance lines) gets merged into the primary inheritance line. """ @export class BaseClassIsNotAMixinError(ExtendedTypeError): pass @export class DuplicateFieldInSlotsError(ExtendedTypeError): """ This exception is raised when a slot name is used multiple times within the inheritance hierarchy. """ @export class AbstractClassError(ExtendedTypeError): """ This exception is raised, when a class contains methods marked with *abstractmethod* or *must-override*. .. seealso:: :func:`@abstractmethod ` |rarr| Mark a method as *abstract*. :func:`@mustoverride ` |rarr| Mark a method as *must overrride*. :exc:`~MustOverrideClassError` |rarr| Exception raised, if a method is marked as *must-override*. """ @export class MustOverrideClassError(AbstractClassError): """ This exception is raised, when a class contains methods marked with *must-override*. .. seealso:: :func:`@abstractmethod ` |rarr| Mark a method as *abstract*. :func:`@mustoverride ` |rarr| Mark a method as *must overrride*. :exc:`~AbstractClassError` |rarr| Exception raised, if a method is marked as *abstract*. """ # """ # Metaclass that allows multiple dispatch of methods based on method signatures. # # .. seealso: # # `Python Cookbook - Multiple dispatch with function annotations `__ # """ M = TypeVar("M", bound=Callable) #: A type variable for methods. @export def slotted(cls): if cls.__class__ is type: metacls = ExtendedType elif issubclass(cls.__class__, ExtendedType): metacls = cls.__class__ for method in cls.__methods__: delattr(method, "__classobj__") else: raise ExtendedTypeError("Class uses an incompatible meta-class.") # FIXME: create exception for it? bases = tuple(base for base in cls.__bases__ if base is not object) slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() members = { "__qualname__": cls.__qualname__ } for key, value in cls.__dict__.items(): if key not in slots: members[key] = value return metacls(cls.__name__, bases, members, slots=True) @export def mixin(cls): if cls.__class__ is type: metacls = ExtendedType elif issubclass(cls.__class__, ExtendedType): metacls = cls.__class__ for method in cls.__methods__: delattr(method, "__classobj__") else: raise ExtendedTypeError("Class uses an incompatible meta-class.") # FIXME: create exception for it? bases = tuple(base for base in cls.__bases__ if base is not object) slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() members = { "__qualname__": cls.__qualname__ } for key, value in cls.__dict__.items(): if key not in slots: members[key] = value return metacls(cls.__name__, bases, members, mixin=True) @export def singleton(cls): if cls.__class__ is type: metacls = ExtendedType elif issubclass(cls.__class__, ExtendedType): metacls = cls.__class__ for method in cls.__methods__: delattr(method, "__classobj__") else: raise ExtendedTypeError("Class uses an incompatible meta-class.") # FIXME: create exception for it? bases = tuple(base for base in cls.__bases__ if base is not object) slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() members = { "__qualname__": cls.__qualname__ } for key, value in cls.__dict__.items(): if key not in slots: members[key] = value return metacls(cls.__name__, bases, members, singleton=True) @export def abstractmethod(method: M) -> M: """ Mark a method as *abstract* and replace the implementation with a new method raising a :exc:`NotImplementedError`. The original method is stored in ``.__wrapped__`` and it's doc-string is copied to the replacing method. In additional field ``.__abstract__`` is added. .. warning:: This decorator should be used in combination with meta-class :class:`~pyTooling.Metaclasses.ExtendedType`. Otherwise, an abstract class itself doesn't throw a :exc:`~pyTooling.Exceptions.AbstractClassError` at instantiation. .. admonition:: ``example.py`` .. code-block:: python class Data(mataclass=ExtendedType): @abstractmethod def method(self) -> bool: '''This method needs to be implemented''' :param method: Method that is marked as *abstract*. :returns: Replacement method, which raises a :exc:`NotImplementedError`. .. seealso:: * :exc:`~pyTooling.Exceptions.AbstractClassError` * :func:`~pyTooling.Metaclasses.mustoverride` * :func:`~pyTooling.Metaclasses.notimplemented` """ @wraps(method) def func(self) -> NoReturn: raise NotImplementedError(f"Method '{method.__name__}' is abstract and needs to be overridden in a derived class.") func.__abstract__ = True return func @export def mustoverride(method: M) -> M: """ Mark a method as *must-override*. The returned function is the original function, but with an additional field ``.____mustOverride__``, so a meta-class can identify a *must-override* method and raise an error. Such an error is not raised if the method is overridden by an inheriting class. A *must-override* methods can offer a partial implementation, which is called via ``super()...``. .. warning:: This decorator needs to be used in combination with meta-class :class:`~pyTooling.Metaclasses.ExtendedType`. Otherwise, an abstract class itself doesn't throw a :exc:`~pyTooling.Exceptions.MustOverrideClassError` at instantiation. .. admonition:: ``example.py`` .. code-block:: python class Data(mataclass=ExtendedType): @mustoverride def method(self): '''This is a very basic implementation''' :param method: Method that is marked as *must-override*. :returns: Same method, but with additional ``.__mustOverride__`` field. .. seealso:: * :exc:`~pyTooling.Exceptions.MustOverrideClassError` * :func:`~pyTooling.Metaclasses.abstractmethod` * :func:`~pyTooling.Metaclasses.notimplemented` """ method.__mustOverride__ = True return method # @export # def overloadable(method: M) -> M: # method.__overloadable__ = True # return method # @export # class DispatchableMethod: # """Represents a single multimethod.""" # # _methods: Dict[Tuple, Callable] # __name__: str # __slots__ = ("_methods", "__name__") # # def __init__(self, name: str) -> None: # self.__name__ = name # self._methods = {} # # def __call__(self, *args: Any): # """Call a method based on type signature of the arguments.""" # types = tuple(type(arg) for arg in args[1:]) # meth = self._methods.get(types, None) # if meth: # return meth(*args) # else: # raise TypeError(f"No matching method for types {types}.") # # def __get__(self, instance, cls): # Starting with Python 3.11+, use typing.Self as return type # """Descriptor method needed to make calls work in a class.""" # if instance is not None: # return MethodType(self, instance) # else: # return self # # def register(self, method: Callable) -> None: # """Register a new method as a dispatchable.""" # # # Build a signature from the method's type annotations # sig = signature(method) # types: List[Type] = [] # # for name, parameter in sig.parameters.items(): # if name == "self": # continue # # if parameter.annotation is Parameter.empty: # raise TypeError(f"Parameter '{name}' in method '{method.__name__}' must be annotated with a type.") # # if not isinstance(parameter.annotation, type): # raise TypeError(f"Parameter '{name}' in method '{method.__name__}' annotation must be a type.") # # if parameter.default is not Parameter.empty: # self._methods[tuple(types)] = method # # types.append(parameter.annotation) # # self._methods[tuple(types)] = method # @export # class DispatchDictionary(dict): # """Special dictionary to build dispatchable methods in a metaclass.""" # # def __setitem__(self, key: str, value: Any): # if callable(value) and key in self: # # If key already exists, it must be a dispatchable method or callable # currentValue = self[key] # if isinstance(currentValue, DispatchableMethod): # currentValue.register(value) # else: # dispatchable = DispatchableMethod(key) # dispatchable.register(currentValue) # dispatchable.register(value) # # super().__setitem__(key, dispatchable) # else: # super().__setitem__(key, value) @export class ExtendedType(type): """ An updates meta-class to construct new classes with an extended feature set. .. todo:: META::ExtendedType Needs documentation. .. todo:: META::ExtendedType allow __dict__ and __weakref__ if slotted is enabled .. rubric:: Features: * Store object members more efficiently in ``__slots__`` instead of ``_dict__``. * Implement ``__slots__`` only on primary inheritance line. * Collect class variables on secondary inheritance lines (mixin-classes) and defer implementation as ``__slots__``. * Handle object state exporting and importing for slots (:mod:`pickle` support) via ``__getstate__``/``__setstate__``. * Allow only a single instance to be created (:term:`singleton`). |br| Further instantiations will return the previously create instance (identical object). * Define methods as :term:`abstract ` or :term:`must-override ` and prohibit instantiation of :term:`abstract classes `. .. #* Allow method overloading and dispatch overloads based on argument signatures. .. rubric:: Added class fields: :__slotted__: True, if class uses `__slots__`. :__allSlots__: Set of class fields stored in slots for all classes in the inheritance hierarchy. :__slots__: Tuple of class fields stored in slots for current class in the inheritance hierarchy. |br| See :pep:`253` for details. :__isMixin__: True, if class is a mixin-class :__mixinSlots__: List of collected slots from secondary inheritance hierarchy (mixin hierarchy). :__methods__: List of methods. :__methodsWithAttributes__: List of methods with pyTooling attributes. :__abstractMethods__: List of abstract methods, which need to be implemented in the next class hierarchy levels. :__isAbstract__: True, if class is abstract. :__isSingleton__: True, if class is a singleton :__singletonInstanceCond__: Condition variable to protect the singleton creation. :__singletonInstanceInit__: Singleton is initialized. :__singletonInstanceCache__: The singleton object, once created. :__pyattr__: List of class attributes. .. rubric:: Added class properties: :HasClassAttributes: Read-only property to check if the class has Attributes. :HasMethodAttributes: Read-only property to check if the class has methods with Attributes. .. rubric:: Added methods: If slots are used, the following methods are added to support :mod:`pickle`: :__getstate__: Export an object's state for serialization. |br| See :pep:`307` for details. :__setstate__: Import an object's state for deserialization. |br| See :pep:`307` for details. .. rubric:: Modified ``__new__`` method: If class is a singleton, ``__new__`` will be replaced by a wrapper method. This wrapper is marked with ``__singleton_wrapper__``. If class is abstract, ``__new__`` will be replaced by a method raising an exception. This replacement is marked with ``__raises_abstract_class_error__``. .. rubric:: Modified ``__init__`` method: If class is a singleton, ``__init__`` will be replaced by a wrapper method. This wrapper is marked by ``__singleton_wrapper__``. .. rubric:: Modified abstract methods: If a method is abstract, its marked with ``__abstract__``. |br| If a method is must override, its marked with ``__mustOverride__``. """ # @classmethod # def __prepare__(cls, className, baseClasses, slots: bool = False, mixin: bool = False, singleton: bool = False): # return DispatchDictionary() def __new__( self, className: str, baseClasses: Tuple[type], members: Dict[str, Any], slots: bool = False, mixin: bool = False, singleton: bool = False ) -> Self: """ Construct a new class using this :term:`meta-class`. :param className: The name of the class to construct. :param baseClasses: The tuple of :term:`base-classes ` the class is derived from. :param members: The dictionary of members for the constructed class. :param slots: If true, store object attributes in :term:`__slots__ ` instead of ``__dict__``. :param mixin: If true, make the class a :term:`Mixin-Class`. If false, create slots if ``slots`` is true. If none, preserve behavior of primary base-class. :param singleton: If true, make the class a :term:`Singleton`. :returns: The new class. :raises AttributeError: If base-class has no '__slots__' attribute. :raises AttributeError: If slot already exists in base-class. """ try: from pyTooling.Attributes import ATTRIBUTES_MEMBER_NAME, AttributeScope except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover from Attributes import ATTRIBUTES_MEMBER_NAME, AttributeScope # Inherit 'slots' feature from primary base-class if len(baseClasses) > 0: primaryBaseClass = baseClasses[0] if isinstance(primaryBaseClass, self): slots = primaryBaseClass.__slotted__ # Compute slots and mixin-slots from annotated fields as well as class- and object-fields with initial values. classFields, objectFields = self._computeSlots(className, baseClasses, members, slots, mixin) # Compute abstract methods abstractMethods, members = self._checkForAbstractMethods(baseClasses, members) # Create a new class newClass = type.__new__(self, className, baseClasses, members) # Apply class fields for fieldName, typeAnnotation in classFields.items(): setattr(newClass, fieldName, typeAnnotation) # Search in inheritance tree for abstract methods newClass.__abstractMethods__ = abstractMethods newClass.__isAbstract__ = self._wrapNewMethodIfAbstract(newClass) newClass.__isSingleton__ = self._wrapNewMethodIfSingleton(newClass, singleton) if slots: # If slots are used, implement __getstate__/__setstate__ API to support serialization using pickle. if "__getstate__" not in members: def __getstate__(self) -> Dict[str, Any]: try: return {slotName: getattr(self, slotName) for slotName in self.__allSlots__} except AttributeError as ex: raise ExtendedTypeError(f"Unassigned field '{ex.name}' in object '{self}' of type '{self.__class__.__name__}'.") from ex newClass.__getstate__ = __getstate__ if "__setstate__" not in members: def __setstate__(self, state: Dict[str, Any]) -> None: if self.__allSlots__ != (slots := set(state.keys())): if len(diff := self.__allSlots__.difference(slots)) > 0: raise ExtendedTypeError(f"""Missing fields in parameter 'state': '{"', '".join(diff)}'""") # WORKAROUND: Python <3.12 else: diff = slots.difference(self.__allSlots__) raise ExtendedTypeError(f"""Unexpected fields in parameter 'state': '{"', '".join(diff)}'""") # WORKAROUND: Python <3.12 for slotName, value in state.items(): setattr(self, slotName, value) newClass.__setstate__ = __setstate__ # Check for inherited class attributes attributes = [] setattr(newClass, ATTRIBUTES_MEMBER_NAME, attributes) for base in baseClasses: if hasattr(base, ATTRIBUTES_MEMBER_NAME): pyAttr = getattr(base, ATTRIBUTES_MEMBER_NAME) for att in pyAttr: if AttributeScope.Class in att.Scope: attributes.append(att) att.__class__._classes.append(newClass) # Check methods for attributes methods, methodsWithAttributes = self._findMethods(newClass, baseClasses, members) # Add new fields for found methods newClass.__methods__ = tuple(methods) newClass.__methodsWithAttributes__ = tuple(methodsWithAttributes) # Additional methods on a class def GetMethodsWithAttributes(self, predicate: Nullable[TAttributeFilter[TAttr]] = None) -> Dict[Callable, Tuple["Attribute", ...]]: """ :param predicate: :return: :raises ValueError: :raises ValueError: """ try: from ..Attributes import Attribute except (ImportError, ModuleNotFoundError): # pragma: no cover try: from Attributes import Attribute except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover raise ex if predicate is None: predicate = Attribute elif isinstance(predicate, Iterable): for attribute in predicate: if not issubclass(attribute, Attribute): raise ValueError(f"Parameter 'predicate' contains an element which is not a sub-class of 'Attribute'.") predicate = tuple(predicate) elif not issubclass(predicate, Attribute): raise ValueError(f"Parameter 'predicate' is not a sub-class of 'Attribute'.") methodAttributePairs = {} for method in newClass.__methodsWithAttributes__: matchingAttributes = [] for attribute in method.__pyattr__: if isinstance(attribute, predicate): matchingAttributes.append(attribute) if len(matchingAttributes) > 0: methodAttributePairs[method] = tuple(matchingAttributes) return methodAttributePairs newClass.GetMethodsWithAttributes = classmethod(GetMethodsWithAttributes) GetMethodsWithAttributes.__qualname__ = f"{className}.{GetMethodsWithAttributes.__name__}" # GetMethods(predicate) -> dict[method, list[attribute]] / generator # GetClassAtrributes -> list[attributes] / generator # MethodHasAttributes(predicate) -> bool # GetAttribute return newClass @classmethod def _findMethods( self, newClass: "ExtendedType", baseClasses: Tuple[type], members: Dict[str, Any] ) -> Tuple[List[MethodType], List[MethodType]]: """ Find methods and methods with :mod:`pyTooling.Attributes`. .. todo:: Describe algorithm. :param newClass: Newly created class instance. :param baseClasses: The tuple of :term:`base-classes ` the class is derived from. :param members: Members of the new class. :return: """ try: from ..Attributes import Attribute except (ImportError, ModuleNotFoundError): # pragma: no cover try: from Attributes import Attribute except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover raise ex # Embedded bind function due to circular dependencies. def bind(instance: object, func: FunctionType, methodName: Nullable[str] = None): if methodName is None: methodName = func.__name__ boundMethod = func.__get__(instance, instance.__class__) setattr(instance, methodName, boundMethod) return boundMethod methods = [] methodsWithAttributes = [] attributeIndex = {} for base in baseClasses: if hasattr(base, "__methodsWithAttributes__"): methodsWithAttributes.extend(base.__methodsWithAttributes__) for memberName, member in members.items(): if isinstance(member, FunctionType): method = newClass.__dict__[memberName] if hasattr(method, "__classobj__") and getattr(method, "__classobj__") is not newClass: raise TypeError(f"Method '{memberName}' is used by multiple classes: {method.__classobj__} and {newClass}.") else: setattr(method, "__classobj__", newClass) def GetAttributes(inst: Any, predicate: Nullable[Type[Attribute]] = None) -> Tuple[Attribute, ...]: results = [] try: for attribute in inst.__pyattr__: # type: Attribute if isinstance(attribute, predicate): results.append(attribute) return tuple(results) except AttributeError: return tuple() method.GetAttributes = bind(method, GetAttributes) methods.append(method) # print(f" convert function: '{memberName}' to method") # print(f" {member}") if "__pyattr__" in member.__dict__: attributes = member.__pyattr__ # type: List[Attribute] if isinstance(attributes, list) and len(attributes) > 0: methodsWithAttributes.append(member) for attribute in attributes: attribute._functions.remove(method) attribute._methods.append(method) # print(f" attributes: {attribute.__class__.__name__}") if attribute not in attributeIndex: attributeIndex[attribute] = [member] else: attributeIndex[attribute].append(member) # else: # print(f" But has no attributes.") # else: # print(f" ?? {memberName}") return methods, methodsWithAttributes @classmethod def _computeSlots( self, className: str, baseClasses: Tuple[type], members: Dict[str, Any], slots: bool, mixin: bool ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ Compute which field are listed in __slots__ and which need to be initialized in an instance or class. .. todo:: Describe algorithm. :param className: The name of the class to construct. :param baseClasses: Tuple of base-classes. :param members: Dictionary of class members. :param slots: True, if the class should setup ``__slots__``. :param mixin: True, if the class should behave as a mixin-class. :returns: A 2-tuple with a dictionary of class members and object members. """ # Compute which field are listed in __slots__ and which need to be initialized in an instance or class. slottedFields = [] classFields = {} objectFields = {} if slots or mixin: # If slots are used, all base classes must use __slots__. for baseClass in self._iterateBaseClasses(baseClasses): # Exclude object as a special case if baseClass is object or baseClass is Generic: continue if not hasattr(baseClass, "__slots__"): ex = BaseClassWithoutSlotsError(f"Base-classes '{baseClass.__name__}' doesn't use '__slots__'.") ex.add_note(f"All base-classes of a class using '__slots__' must use '__slots__' itself.") raise ex # FIXME: should have a check for non-empty slots on secondary base-classes too # Copy all field names from primary base-class' __slots__, which are later needed for error checking. inheritedSlottedFields = {} if len(baseClasses) > 0: for base in reversed(baseClasses[0].mro()): # Exclude object as a special case if base is object or base is Generic: continue for annotation in base.__slots__: inheritedSlottedFields[annotation] = base # When adding annotated fields to slottedFields, check if name was not used in inheritance hierarchy. if "__annotations__" in members: # WORKAROUND: LEGACY SUPPORT Python <= 3.13 # Accessing annotations was changed in Python 3.14. # The necessary 'annotationlib' is not available for older Python versions. annotations: Dict[str, Any] = members.get("__annotations__", {}) elif version_info >= (3, 14) and (annotate := members.get("__annotate_func__", None)) is not None: from annotationlib import Format annotations: Dict[str, Any] = annotate(Format.VALUE) else: annotations = {} for fieldName, typeAnnotation in annotations.items(): if fieldName in inheritedSlottedFields: cls = inheritedSlottedFields[fieldName] raise AttributeError(f"Slot '{fieldName}' already exists in base-class '{cls.__module__}.{cls.__name__}'.") # If annotated field is a ClassVar, and it has an initial value # * copy field and initial value to classFields dictionary # * remove field from members if isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar and fieldName in members: classFields[fieldName] = members[fieldName] del members[fieldName] # If an annotated field has an initial value # * copy field and initial value to objectFields dictionary # * remove field from members elif fieldName in members: slottedFields.append(fieldName) objectFields[fieldName] = members[fieldName] del members[fieldName] else: slottedFields.append(fieldName) mixinSlots = self._aggregateMixinSlots(className, baseClasses) else: # When adding annotated fields to slottedFields, check if name was not used in inheritance hierarchy. annotations: Dict[str, Any] = members.get("__annotations__", {}) for fieldName, typeAnnotation in annotations.items(): # If annotated field is a ClassVar, and it has an initial value # * copy field and initial value to classFields dictionary # * remove field from members if isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar and fieldName in members: classFields[fieldName] = members[fieldName] del members[fieldName] # FIXME: search for fields without annotation if mixin: mixinSlots.extend(slottedFields) members["__slotted__"] = True members["__slots__"] = tuple() members["__allSlots__"] = set() members["__isMixin__"] = True members["__mixinSlots__"] = tuple(mixinSlots) elif slots: slottedFields.extend(mixinSlots) members["__slotted__"] = True members["__slots__"] = tuple(slottedFields) members["__allSlots__"] = set(chain(slottedFields, inheritedSlottedFields.keys())) members["__isMixin__"] = False members["__mixinSlots__"] = tuple() else: members["__slotted__"] = False # NO __slots__ # members["__allSlots__"] = set() members["__isMixin__"] = False members["__mixinSlots__"] = tuple() return classFields, objectFields @classmethod def _aggregateMixinSlots(self, className: str, baseClasses: Tuple[type]) -> List[str]: """ Aggregate slot names requested by mixin-base-classes. .. todo:: Describe algorithm. :param className: The name of the class to construct. :param baseClasses: The tuple of :term:`base-classes ` the class is derived from. :returns: A list of slot names. """ mixinSlots = [] if len(baseClasses) > 0: # If class has base-classes ensure only the primary inheritance path uses slots and all secondary inheritance # paths have an empty slots tuple. Otherwise, raise a BaseClassWithNonEmptySlotsError. inheritancePaths = [path for path in self._iterateBaseClassPaths(baseClasses)] primaryInharitancePath: Set[type] = set(inheritancePaths[0]) for typePath in inheritancePaths[1:]: for t in typePath: if hasattr(t, "__slots__") and len(t.__slots__) != 0 and t not in primaryInharitancePath: ex = BaseClassWithNonEmptySlotsError(f"Base-class '{t.__name__}' has non-empty __slots__ and can't be used as a direct or indirect base-class for '{className}'.") ex.add_note(f"In Python, only one inheritance branch can use non-empty __slots__.") # ex.add_note(f"With ExtendedType, only the primary base-class can use non-empty __slots__.") # ex.add_note(f"Secondary base-classes should be marked as mixin-classes.") raise ex # If current class is set to be a mixin, then aggregate all mixinSlots in a list. # Ensure all base-classes are either constructed # * by meta-class ExtendedType, or # * use no slots, or # * are typing.Generic # If it was constructed by ExtendedType, then ensure this class itself is a mixin-class. for baseClass in baseClasses: # type: ExtendedType if isinstance(baseClass, _GenericAlias) and baseClass.__origin__ is Generic: pass elif baseClass.__class__ is self and baseClass.__isMixin__: mixinSlots.extend(baseClass.__mixinSlots__) elif hasattr(baseClass, "__mixinSlots__"): mixinSlots.extend(baseClass.__mixinSlots__) return mixinSlots @classmethod def _iterateBaseClasses(metacls, baseClasses: Tuple[type]) -> Generator[type, None, None]: """ Return a generator to iterate (visit) all base-classes ... .. todo:: Describe iteration order. :param baseClasses: The tuple of :term:`base-classes ` the class is derived from. :returns: Generator to iterate all base-classes. """ if len(baseClasses) == 0: return visited: Set[type] = set() iteratorStack: List[Iterator[type]] = list() for baseClass in baseClasses: yield baseClass visited.add(baseClass) iteratorStack.append(iter(baseClass.__bases__)) while True: try: base = next(iteratorStack[-1]) # type: type if base not in visited: yield base if len(base.__bases__) > 0: iteratorStack.append(iter(base.__bases__)) else: continue except StopIteration: iteratorStack.pop() if len(iteratorStack) == 0: break @classmethod def _iterateBaseClassPaths(metacls, baseClasses: Tuple[type]) -> Generator[Tuple[type, ...], None, None]: """ Return a generator to iterate all possible inheritance paths for a given list of base-classes. An inheritance path is expressed as a tuple of base-classes from current base-class (left-most item) to :class:`object` (right-most item). :param baseClasses: The tuple of :term:`base-classes ` the class is derived from. :returns: Generator to iterate all inheritance paths. |br| An inheritance path is a tuple of types (base-classes). """ if len(baseClasses) == 0: return typeStack: List[type] = list() iteratorStack: List[Iterator[type]] = list() for baseClass in baseClasses: typeStack.append(baseClass) iteratorStack.append(iter(baseClass.__bases__)) while True: try: base = next(iteratorStack[-1]) # type: type typeStack.append(base) if len(base.__bases__) == 0: yield tuple(typeStack) typeStack.pop() else: iteratorStack.append(iter(base.__bases__)) except StopIteration: typeStack.pop() iteratorStack.pop() if len(typeStack) == 0: break @classmethod def _checkForAbstractMethods(metacls, baseClasses: Tuple[type], members: Dict[str, Any]) -> Tuple[Dict[str, Callable], Dict[str, Any]]: """ Check if the current class contains abstract methods and return a tuple of them. These abstract methods might be inherited from any base-class. If there are inherited abstract methods, check if they are now implemented (overridden) by the current class that's right now constructed. :param baseClasses: The tuple of :term:`base-classes ` the class is derived from. :param members: The dictionary of members for the constructed class. :returns: A tuple of abstract method's names. """ abstractMethods = {} if baseClasses: # Aggregate all abstract methods from all base-classes. for baseClass in baseClasses: if hasattr(baseClass, "__abstractMethods__"): abstractMethods.update(baseClass.__abstractMethods__) for base in baseClasses: for memberName, member in base.__dict__.items(): if (memberName in abstractMethods and isinstance(member, FunctionType) and not (hasattr(member, "__abstract__") or hasattr(member, "__mustOverride__"))): def outer(method): @wraps(method) def inner(cls, *args: Any, **kwargs: Any): return method(cls, *args, **kwargs) return inner members[memberName] = outer(member) # Check if methods are marked: # * If so, add them to list of abstract methods # * If not, method is now implemented and removed from list for memberName, member in members.items(): if callable(member): if ((hasattr(member, "__abstract__") and member.__abstract__) or (hasattr(member, "__mustOverride__") and member.__mustOverride__)): abstractMethods[memberName] = member elif memberName in abstractMethods: del abstractMethods[memberName] return abstractMethods, members @classmethod def _wrapNewMethodIfSingleton(metacls, newClass, singleton: bool) -> bool: """ If a class is a singleton, wrap the ``_new__`` method, so it returns a cached object, if a first object was created. Only the first object creation initializes the object. This implementation is threadsafe. :param newClass: The newly constructed class for further modifications. :param singleton: If ``True``, the class allows only a single instance to exist. :returns: ``True``, if the class is a singleton. """ if hasattr(newClass, "__isSingleton__"): singleton = newClass.__isSingleton__ if singleton: oldnew = newClass.__new__ if hasattr(oldnew, "__singleton_wrapper__"): oldnew = oldnew.__wrapped__ oldinit = newClass.__init__ if hasattr(oldinit, "__singleton_wrapper__"): oldinit = oldinit.__wrapped__ @wraps(oldnew) def singleton_new(cls, *args: Any, **kwargs: Any): with cls.__singletonInstanceCond__: if cls.__singletonInstanceCache__ is None: obj = oldnew(cls, *args, **kwargs) cls.__singletonInstanceCache__ = obj else: obj = cls.__singletonInstanceCache__ return obj @wraps(oldinit) def singleton_init(self, *args: Any, **kwargs: Any): cls = self.__class__ cv = cls.__singletonInstanceCond__ with cv: if cls.__singletonInstanceInit__: oldinit(self, *args, **kwargs) cls.__singletonInstanceInit__ = False cv.notify_all() elif args or kwargs: raise ValueError(f"A further instance of a singleton can't be reinitialized with parameters.") else: while cls.__singletonInstanceInit__: cv.wait() singleton_new.__singleton_wrapper__ = True singleton_init.__singleton_wrapper__ = True newClass.__new__ = singleton_new newClass.__init__ = singleton_init newClass.__singletonInstanceCond__ = Condition() newClass.__singletonInstanceInit__ = True newClass.__singletonInstanceCache__ = None return True return False @classmethod def _wrapNewMethodIfAbstract(metacls, newClass) -> bool: """ If the class has abstract methods, replace the ``_new__`` method, so it raises an exception. :param newClass: The newly constructed class for further modifications. :returns: ``True``, if the class is abstract. :raises AbstractClassError: If the class is abstract and can't be instantiated. """ # Replace '__new__' by a variant to throw an error on not overridden methods if len(newClass.__abstractMethods__) > 0: oldnew = newClass.__new__ if hasattr(oldnew, "__raises_abstract_class_error__"): oldnew = oldnew.__wrapped__ @wraps(oldnew) def abstract_new(cls, *_, **__): raise AbstractClassError(f"""Class '{cls.__name__}' is abstract. The following methods: '{"', '".join(newClass.__abstractMethods__)}' need to be overridden in a derived class.""") abstract_new.__raises_abstract_class_error__ = True newClass.__new__ = abstract_new return True # Handle classes which are not abstract, especially derived classes, if not abstract anymore else: # skip intermediate 'new' function if class isn't abstract anymore try: if newClass.__new__.__raises_abstract_class_error__: origNew = newClass.__new__.__wrapped__ # WORKAROUND: __new__ checks tp_new and implements different behavior # Bugreport: https://github.com/python/cpython/issues/105888 if origNew is object.__new__: @wraps(object.__new__) def wrapped_new(inst, *_, **__): return object.__new__(inst) newClass.__new__ = wrapped_new else: newClass.__new__ = origNew elif newClass.__new__.__isSingleton__: raise Exception(f"Found a singleton wrapper around an AbstractError raising method. This case is not handled yet.") except AttributeError as ex: # WORKAROUND: # AttributeError.name was added in Python 3.10. For version <3.10 use a string contains operation. try: if ex.name != "__raises_abstract_class_error__": raise ex except AttributeError: if "__raises_abstract_class_error__" not in str(ex): raise ex return False # Additional properties and methods on a class @readonly def HasClassAttributes(self) -> bool: """ Read-only property to check if the class has Attributes (:attr:`__pyattr__`). :returns: ``True``, if the class has Attributes. """ try: return len(self.__pyattr__) > 0 except AttributeError: return False @readonly def HasMethodAttributes(self) -> bool: """ Read-only property to check if the class has methods with Attributes (:attr:`__methodsWithAttributes__`). :returns: ``True``, if the class has any method with Attributes. """ try: return len(self.__methodsWithAttributes__) > 0 except AttributeError: return False @export class SlottedObject(metaclass=ExtendedType, slots=True): """Classes derived from this class will store all members in ``__slots__``.""" pyTooling-8.11.0/pyTooling/Packaging/000077500000000000000000000000001513317154500174425ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Packaging/__init__.py000066400000000000000000001217031513317154500215570ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ __ _ ___| | ____ _ __ _(_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_) / _` |/ __| |/ / _` |/ _` | | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| __/ (_| | (__| < (_| | (_| | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| \__,_|\___|_|\_\__,_|\__, |_|_| |_|\__, | # # |_| |___/ |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ A set of helper functions to describe a Python package for setuptools. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from ast import parse as ast_parse, iter_child_nodes, Assign, Constant, Name, List as ast_List from collections.abc import Sized from os import scandir as os_scandir from pathlib import Path from re import split as re_split from sys import version_info from typing import List, Iterable, Dict, Sequence, Any, Optional as Nullable, Union, Tuple try: from pyTooling.Decorators import export, readonly from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType from pyTooling.Common import __version__, getFullyQualifiedName, firstElement from pyTooling.Licensing import License, Apache_2_0_License except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Packaging] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from Exceptions import ToolingException from MetaClasses import ExtendedType from Common import __version__, getFullyQualifiedName, firstElement from Licensing import License, Apache_2_0_License except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Packaging] Could not import directly!") raise ex __all__ = [ "STATUS", "DEFAULT_LICENSE", "DEFAULT_PY_VERSIONS", "DEFAULT_CLASSIFIERS", "DEFAULT_README", "DEFAULT_REQUIREMENTS", "DEFAULT_DOCUMENTATION_REQUIREMENTS", "DEFAULT_TEST_REQUIREMENTS", "DEFAULT_PACKAGING_REQUIREMENTS", "DEFAULT_VERSION_FILE" ] @export class Readme: """Encapsulates the READMEs file content and MIME type.""" _content: str #: Content of the README file _mimeType: str #: MIME type of the README content def __init__(self, content: str, mimeType: str) -> None: """ Initializes a README file wrapper. :param content: Raw content of the README file. :param mimeType: MIME type of the README file. """ self._content = content self._mimeType = mimeType @readonly def Content(self) -> str: """ Read-only property to access the README's content. :returns: Raw content of the README file. """ return self._content @readonly def MimeType(self) -> str: """ Read-only property to access the README's MIME type. :returns: The MIME type of the README file. """ return self._mimeType @export def loadReadmeFile(readmeFile: Path) -> Readme: """ Read the README file (e.g. in Markdown format), so it can be used as long description for the package. Supported formats: * Plain text (``*.txt``) * Markdown (``*.md``) * ReStructured Text (``*.rst``) :param readmeFile: Path to the `README` file as an instance of :class:`Path`. :returns: A tuple containing the file content and the MIME type. :raises TypeError: If parameter 'readmeFile' is not of type 'Path'. :raises ValueError: If README file has an unsupported format. :raises FileNotFoundError: If README file does not exist. """ if not isinstance(readmeFile, Path): ex = TypeError(f"Parameter 'readmeFile' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(readmeFile)}'.") raise ex if readmeFile.suffix == ".txt": mimeType = "text/plain" elif readmeFile.suffix == ".md": mimeType = "text/markdown" elif readmeFile.suffix == ".rst": mimeType = "text/x-rst" else: # pragma: no cover raise ValueError("Unsupported README format.") try: with readmeFile.open("r", encoding="utf-8") as file: return Readme( content=file.read(), mimeType=mimeType ) except FileNotFoundError as ex: raise FileNotFoundError(f"README file '{readmeFile}' not found in '{Path.cwd()}'.") from ex @export def loadRequirementsFile(requirementsFile: Path, indent: int = 0, debug: bool = False) -> List[str]: """ Reads a `requirements.txt` file (recursively) and extracts all specified dependencies into an array. Special dependency entries like Git repository references are translates to match the syntax expected by setuptools. .. hint:: Duplicates should be removed by converting the result to a :class:`set` and back to a :class:`list`. .. code-block:: Python requirements = list(set(loadRequirementsFile(requirementsFile))) :param requirementsFile: Path to the ``requirements.txt`` file as an instance of :class:`Path`. :param debug: If ``True``, print found dependencies and recursion. :returns: A list of dependencies. :raises TypeError: If parameter 'requirementsFile' is not of type 'Path'. :raises FileNotFoundError: If requirements file does not exist. """ if not isinstance(requirementsFile, Path): ex = TypeError(f"Parameter '{requirementsFile}' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(requirementsFile)}'.") raise ex def _loadRequirementsFile(requirementsFile: Path, indent: int) -> List[str]: """Recursive variant of :func:`loadRequirementsFile`.""" requirements = [] try: with requirementsFile.open("r", encoding="utf-8") as file: if debug: print(f"[pyTooling.Packaging]{' ' * indent} Extracting requirements from '{requirementsFile}'.") for line in file.readlines(): line = line.strip() if line.startswith("#") or line == "": continue elif line.startswith("-r"): # Remove the first word/argument (-r) filename = line[2:].lstrip() requirements += _loadRequirementsFile(requirementsFile.parent / filename, indent + 1) elif line.startswith("https"): if debug: print(f"[pyTooling.Packaging]{' ' * indent} Found URL '{line}'.") # Convert 'URL#NAME' to 'NAME @ URL' splitItems = line.split("#") requirements.append(f"{splitItems[1]} @ {splitItems[0]}") else: if debug: print(f"[pyTooling.Packaging]{' ' * indent} - {line}") requirements.append(line) except FileNotFoundError as ex: raise FileNotFoundError(f"Requirements file '{requirementsFile}' not found in '{Path.cwd()}'.") from ex return requirements return _loadRequirementsFile(requirementsFile, 0) @export class VersionInformation(metaclass=ExtendedType, slots=True): """Encapsulates version information extracted from a Python source file.""" _author: str #: Author name(s). _copyright: str #: Copyright information. _email: str #: Author's email address. _keywords: List[str] #: Keywords. _license: str #: License name. _description: str #: Description of the package. _version: str #: Version number. def __init__( self, author: str, email: str, copyright: str, license: str, version: str, description: str, keywords: Iterable[str] ) -> None: """ Initializes a Python package (version) information instance. :param author: Author of the Python package. :param email: The author's email address :param copyright: The copyright notice of the Package. :param license: The Python package's license. :param version: The Python package's version. :param description: The Python package's short description. :param keywords: The Python package's list of keywords. """ self._author = author self._email = email self._copyright = copyright self._license = license self._version = version self._description = description self._keywords = [k for k in keywords] @readonly def Author(self) -> str: """Name(s) of the package author(s).""" return self._author @readonly def Copyright(self) -> str: """Copyright information.""" return self._copyright @readonly def Description(self) -> str: """Package description text.""" return self._description @readonly def Email(self) -> str: """Email address of the author.""" return self._email @readonly def Keywords(self) -> List[str]: """List of keywords.""" return self._keywords @readonly def License(self) -> str: """License name.""" return self._license @readonly def Version(self) -> str: """Version number.""" return self._version def __str__(self) -> str: return f"{self._version}" @export def extractVersionInformation(sourceFile: Path) -> VersionInformation: """ Extract double underscored variables from a Python source file, so these can be used for single-sourcing information. Supported variables: * ``__author__`` * ``__copyright__`` * ``__email__`` * ``__keywords__`` * ``__license__`` * ``__version__`` :param sourceFile: Path to a Python source file as an instance of :class:`Path`. :returns: An instance of :class:`VersionInformation` with gathered variable contents. :raises TypeError: If parameter 'sourceFile' is not of type :class:`~pathlib.Path`. """ if not isinstance(sourceFile, Path): ex = TypeError(f"Parameter 'sourceFile' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(sourceFile)}'.") raise ex _author = None _copyright = None _description = "" _email = None _keywords = [] _license = None _version = None try: with sourceFile.open("r", encoding="utf-8") as file: content = file.read() except FileNotFoundError as ex: raise FileNotFoundError try: ast = ast_parse(content) except Exception as ex: # pragma: no cover raise ToolingException(f"Internal error when parsing '{sourceFile}'.") from ex for item in iter_child_nodes(ast): if isinstance(item, Assign) and len(item.targets) == 1: target = item.targets[0] value = item.value if isinstance(target, Name) and target.id == "__author__": if isinstance(value, Constant) and isinstance(value.value, str): _author = value.value if isinstance(target, Name) and target.id == "__copyright__": if isinstance(value, Constant) and isinstance(value.value, str): _copyright = value.value if isinstance(target, Name) and target.id == "__email__": if isinstance(value, Constant) and isinstance(value.value, str): _email = value.value if isinstance(target, Name) and target.id == "__keywords__": if isinstance(value, Constant) and isinstance(value.value, str): # pragma: no cover raise TypeError(f"Variable '__keywords__' should be a list of strings.") elif isinstance(value, ast_List): for const in value.elts: if isinstance(const, Constant) and isinstance(const.value, str): _keywords.append(const.value) else: # pragma: no cover raise TypeError(f"List elements in '__keywords__' should be strings.") else: # pragma: no cover raise TypeError(f"Used unsupported type for variable '__keywords__'.") if isinstance(target, Name) and target.id == "__license__": if isinstance(value, Constant) and isinstance(value.value, str): _license = value.value if isinstance(target, Name) and target.id == "__version__": if isinstance(value, Constant) and isinstance(value.value, str): _version = value.value if _author is None: raise AssertionError(f"Could not extract '__author__' from '{sourceFile}'.") # pragma: no cover if _copyright is None: raise AssertionError(f"Could not extract '__copyright__' from '{sourceFile}'.") # pragma: no cover if _email is None: raise AssertionError(f"Could not extract '__email__' from '{sourceFile}'.") # pragma: no cover if _license is None: raise AssertionError(f"Could not extract '__license__' from '{sourceFile}'.") # pragma: no cover if _version is None: raise AssertionError(f"Could not extract '__version__' from '{sourceFile}'.") # pragma: no cover return VersionInformation(_author, _email, _copyright, _license, _version, _description, _keywords) STATUS: Dict[str, str] = { "planning": "1 - Planning", "pre-alpha": "2 - Pre-Alpha", "alpha": "3 - Alpha", "beta": "4 - Beta", "stable": "5 - Production/Stable", "mature": "6 - Mature", "inactive": "7 - Inactive" } """ A dictionary of supported development status values. The mapping's value will be appended to ``Development Status :: `` to form a package classifier. 1. Planning 2. Pre-Alpha 3. Alpha 4. Beta 5. Production/Stable 6. Mature 7. Inactive .. seealso:: `Python package classifiers `__ """ DEFAULT_LICENSE = Apache_2_0_License """ Default license (Apache License, 2.0) used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` if parameter ``license`` is not assigned. """ DEFAULT_PY_VERSIONS = ("3.10", "3.11", "3.12", "3.13", "3.14") """ A tuple of supported CPython versions used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` if parameter ``pythonVersions`` is not assigned. .. seealso:: `Status of Python versions `__ """ DEFAULT_CLASSIFIERS = ( "Operating System :: OS Independent", "Intended Audience :: Developers", "Topic :: Utilities" ) """ A list of Python package classifiers used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` if parameter ``classifiers`` is not assigned. .. seealso:: `Python package classifiers `__ """ DEFAULT_README = Path("README.md") """ Path to the README file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` if parameter ``readmeFile`` is not assigned. """ DEFAULT_REQUIREMENTS = Path("requirements.txt") """ Path to the requirements file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` if parameter ``requirementsFile`` is not assigned. """ DEFAULT_DOCUMENTATION_REQUIREMENTS = Path("doc/requirements.txt") """ Path to the README requirements file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` if parameter ``documentationRequirementsFile`` is not assigned. """ DEFAULT_TEST_REQUIREMENTS = Path("tests/requirements.txt") """ Path to the README requirements file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` if parameter ``unittestRequirementsFile`` is not assigned. """ DEFAULT_PACKAGING_REQUIREMENTS = Path("build/requirements.txt") """ Path to the package requirements file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` if parameter ``packagingRequirementsFile`` is not assigned. """ DEFAULT_VERSION_FILE = Path("__init__.py") @export def DescribePythonPackage( packageName: str, description: str, projectURL: str, sourceCodeURL: str, documentationURL: str, issueTrackerCodeURL: str, keywords: Iterable[str] = None, license: License = DEFAULT_LICENSE, readmeFile: Path = DEFAULT_README, requirementsFile: Path = DEFAULT_REQUIREMENTS, documentationRequirementsFile: Path = DEFAULT_DOCUMENTATION_REQUIREMENTS, unittestRequirementsFile: Path = DEFAULT_TEST_REQUIREMENTS, packagingRequirementsFile: Path = DEFAULT_PACKAGING_REQUIREMENTS, additionalRequirements: Dict[str, List[str]] = None, sourceFileWithVersion: Nullable[Path] = DEFAULT_VERSION_FILE, classifiers: Iterable[str] = DEFAULT_CLASSIFIERS, developmentStatus: str = "stable", pythonVersions: Sequence[str] = DEFAULT_PY_VERSIONS, consoleScripts: Dict[str, str] = None, dataFiles: Dict[str, List[str]] = None, debug: bool = False ) -> Dict[str, Any]: """ Helper function to describe a Python package. .. hint:: Some information will be gathered automatically from well-known files. Examples: ``README.md``, ``requirements.txt``, ``__init__.py`` .. topic:: Handling of namespace packages If parameter ``packageName`` contains a dot, a namespace package is assumed. Then :func:`setuptools.find_namespace_packages` is used to discover package files. |br| Otherwise, the package is considered a normal package and :func:`setuptools.find_packages` is used. In both cases, the following packages (directories) are excluded from search: * ``build``, ``build.*`` * ``dist``, ``dist.*`` * ``doc``, ``doc.*`` * ``tests``, ``tests.*`` .. topic:: Handling of minimal Python version The minimal required Python version is selected from parameter ``pythonVersions``. .. topic:: Handling of dunder variables A Python source file specified by parameter ``sourceFileWithVersion`` will be analyzed with Pythons parser and the resulting AST will be searched for the following dunder variables: * ``__author__``: :class:`str` * ``__copyright__``: :class:`str` * ``__email__``: :class:`str` * ``__keywords__``: :class:`typing.Iterable`[:class:`str`] * ``__license__``: :class:`str` * ``__version__``: :class:`str` The gathered information be used to add further mappings in the result dictionary. .. topic:: Handling of package classifiers To reduce redundantly provided parameters to this function (e.g. supported ``pythonVersions``), only additional classifiers should be provided via parameter ``classifiers``. The supported Python versions will be implicitly converted to package classifiers, so no need to specify them in parameter ``classifiers``. The following classifiers are implicitly handled: license The license specified by parameter ``license`` is translated into a classifier. |br| See also :meth:`pyTooling.Licensing.License.PythonClassifier` Python versions Always add ``Programming Language :: Python :: 3 :: Only``. |br| For each value in ``pythonVersions``, one ``Programming Language :: Python :: Major.Minor`` is added. Development status The development status specified by parameter ``developmentStatus`` is translated to a classifier and added. .. topic:: Handling of extra requirements If additional requirement files are provided, e.g. requirements to build the documentation, then *extra* requirements are defined. These can be installed via ``pip install packageName[extraName]``. If so, an extra called ``all`` is added, so developers can install all dependencies needed for package development. ``doc`` If parameter ``documentationRequirementsFile`` is present, an extra requirements called ``doc`` will be defined. ``test`` If parameter ``unittestRequirementsFile`` is present, an extra requirements called ``test`` will be defined. ``build`` If parameter ``packagingRequirementsFile`` is present, an extra requirements called ``build`` will be defined. User-defined If parameter ``additionalRequirements`` is present, an extra requirements for every mapping entry in the dictionary will be added. ``all`` If any of the above was added, an additional extra requirement called ``all`` will be added, summarizing all extra requirements. .. topic:: Handling of keywords If parameter ``keywords`` is not specified, the dunder variable ``__keywords__`` from ``sourceFileWithVersion`` will be used. Otherwise, the content of the parameter, if not None or empty. :param packageName: Name of the Python package. :param description: Short description of the package. The long description will be read from README file. :param projectURL: URL to the Python project. :param sourceCodeURL: URL to the Python source code. :param documentationURL: URL to the package's documentation. :param issueTrackerCodeURL: URL to the projects issue tracker (ticket system). :param keywords: A list of keywords. :param license: The package's license. (Default: ``Apache License, 2.0``, see :const:`DEFAULT_LICENSE`) :param readmeFile: The path to the README file. (Default: ``README.md``, see :const:`DEFAULT_README`) :param requirementsFile: The path to the project's requirements file. (Default: ``requirements.txt``, see :const:`DEFAULT_REQUIREMENTS`) :param documentationRequirementsFile: The path to the project's requirements file for documentation. (Default: ``doc/requirements.txt``, see :const:`DEFAULT_DOCUMENTATION_REQUIREMENTS`) :param unittestRequirementsFile: The path to the project's requirements file for unit tests. (Default: ``tests/requirements.txt``, see :const:`DEFAULT_TEST_REQUIREMENTS`) :param packagingRequirementsFile: The path to the project's requirements file for packaging. (Default: ``build/requirements.txt``, see :const:`DEFAULT_PACKAGING_REQUIREMENTS`) :param additionalRequirements: A dictionary of a lists with additional requirements. (default: None) :param sourceFileWithVersion: The path to the project's source file containing dunder variables like ``__version__``. (Default: ``__init__.py``, see :const:`DEFAULT_VERSION_FILE`) :param classifiers: A list of package classifiers. (Default: 3 classifiers, see :const:`DEFAULT_CLASSIFIERS`) :param developmentStatus: Development status of the package. (Default: stable, see :const:`STATUS` for supported status values) :param pythonVersions: A list of supported Python 3 version. (Default: all currently maintained CPython versions, see :const:`DEFAULT_PY_VERSIONS`) :param consoleScripts: A dictionary mapping command line names to entry points. (Default: None) :param dataFiles: A dictionary mapping package names to lists of additional data files. :param debug: Enable extended outputs for debugging. :returns: A dictionary suitable for :func:`setuptools.setup`. :raises ToolingException: If package 'setuptools' is not available. :raises TypeError: If parameter 'readmeFile' is not of type :class:`~pathlib.Path`. :raises FileNotFoundError: If README file doesn't exist. :raises TypeError: If parameter 'requirementsFile' is not of type :class:`~pathlib.Path`. :raises FileNotFoundError: If requirements file doesn't exist. :raises TypeError: If parameter 'documentationRequirementsFile' is not of type :class:`~pathlib.Path`. :raises TypeError: If parameter 'unittestRequirementsFile' is not of type :class:`~pathlib.Path`. :raises TypeError: If parameter 'packagingRequirementsFile' is not of type :class:`~pathlib.Path`. :raises TypeError: If parameter 'sourceFileWithVersion' is not of type :class:`~pathlib.Path`. :raises FileNotFoundError: If package file with dunder variables doesn't exist. :raises TypeError: If parameter 'license' is not of type :class:`~pyTooling.Licensing.License`. :raises ValueError: If developmentStatus uses an unsupported value. (See :const:`STATUS`) :raises ValueError: If the content type of the README file is not supported. (See :func:`loadReadmeFile`) :raises FileNotFoundError: If the README file doesn't exist. (See :func:`loadReadmeFile`) :raises FileNotFoundError: If the requirements file doesn't exist. (See :func:`loadRequirementsFile`) """ try: from setuptools import find_packages, find_namespace_packages except ImportError as ex: raise Exception(f"Optional dependency 'setuptools' not installed. Either install pyTooling with extra dependencies 'pyTooling[packaging]' or install 'setuptools' directly.") from ex print(f"[pyTooling.Packaging] Python: {version_info.major}.{version_info.minor}.{version_info.micro}, pyTooling: {__version__}") # Read README for upload to PyPI if not isinstance(readmeFile, Path): ex = TypeError(f"Parameter 'readmeFile' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(readmeFile)}'.") raise ex elif not readmeFile.exists(): raise FileNotFoundError(f"README file '{readmeFile}' not found in '{Path.cwd()}'.") else: readme = loadReadmeFile(readmeFile) # Read requirements file and add them to package dependency list (remove duplicates) if not isinstance(requirementsFile, Path): ex = TypeError(f"Parameter 'requirementsFile' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(requirementsFile)}'.") raise ex elif not requirementsFile.exists(): raise FileNotFoundError(f"Requirements file '{requirementsFile}' not found in '{Path.cwd()}'.") else: requirements = list(set(loadRequirementsFile(requirementsFile, debug=debug))) extraRequirements: Dict[str, List[str]] = {} if documentationRequirementsFile is not None: if not isinstance(documentationRequirementsFile, Path): ex = TypeError(f"Parameter 'documentationRequirementsFile' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(documentationRequirementsFile)}'.") raise ex elif not documentationRequirementsFile.exists(): if debug: print(f"[pyTooling.Packaging] Documentation requirements file '{documentationRequirementsFile}' not found in '{Path.cwd()}'.") print( "[pyTooling.Packaging] No section added to 'extraRequirements'.") # raise FileNotFoundError(f"Documentation requirements file '{documentationRequirementsFile}' not found in '{Path.cwd()}'.") else: extraRequirements["doc"] = list(set(loadRequirementsFile(documentationRequirementsFile, debug=debug))) if unittestRequirementsFile is not None: if not isinstance(unittestRequirementsFile, Path): ex = TypeError(f"Parameter 'unittestRequirementsFile' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(unittestRequirementsFile)}'.") raise ex elif not unittestRequirementsFile.exists(): if debug: print(f"[pyTooling.Packaging] Unit testing requirements file '{unittestRequirementsFile}' not found in '{Path.cwd()}'.") print( "[pyTooling.Packaging] No section added to 'extraRequirements'.") # raise FileNotFoundError(f"Unit testing requirements file '{unittestRequirementsFile}' not found in '{Path.cwd()}'.") else: extraRequirements["test"] = list(set(loadRequirementsFile(unittestRequirementsFile, debug=debug))) if packagingRequirementsFile is not None: if not isinstance(packagingRequirementsFile, Path): ex = TypeError(f"Parameter 'packagingRequirementsFile' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(packagingRequirementsFile)}'.") raise ex elif not packagingRequirementsFile.exists(): if debug: print(f"[pyTooling.Packaging] Packaging requirements file '{packagingRequirementsFile}' not found in '{Path.cwd()}'.") print( "[pyTooling.Packaging] No section added to 'extraRequirements'.") # raise FileNotFoundError(f"Packaging requirements file '{packagingRequirementsFile}' not found in '{Path.cwd()}'.") else: extraRequirements["build"] = list(set(loadRequirementsFile(packagingRequirementsFile, debug=debug))) if additionalRequirements is not None: for key, value in additionalRequirements.items(): extraRequirements[key] = value if len(extraRequirements) > 0: extraRequirements["all"] = list(set([dep for deps in extraRequirements.values() for dep in deps])) # Read __author__, __email__, __version__ from source file if not isinstance(sourceFileWithVersion, Path): ex = TypeError(f"Parameter 'sourceFileWithVersion' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(sourceFileWithVersion)}'.") raise ex elif not sourceFileWithVersion.exists(): raise FileNotFoundError(f"Package file '{sourceFileWithVersion}' with dunder variables not found in '{Path.cwd()}'.") else: versionInformation = extractVersionInformation(sourceFileWithVersion) # Scan for packages and source files if debug: print(f"[pyTooling.Packaging] Exclude list for find_(namespace_)packages:") exclude = [] rootNamespace = firstElement(packageName.split(".")) for dirName in (dirItem.name for dirItem in os_scandir(Path.cwd()) if dirItem.is_dir() and "." not in dirItem.name and dirItem.name != rootNamespace): exclude.append(f"{dirName}") exclude.append(f"{dirName}.*") if debug: print(f"[pyTooling.Packaging] - {dirName}, {dirName}.*") if "." in packageName: exclude.append(rootNamespace) packages = find_namespace_packages(exclude=exclude) if packageName.endswith(".*"): packageName = packageName[:-2] else: packages = find_packages(exclude=exclude) if debug: print(f"[pyTooling.Packaging] Found packages: ({packages.__class__.__name__})") for package in packages: print(f"[pyTooling.Packaging] - {package}") if keywords is None or isinstance(keywords, Sized) and len(keywords) == 0: keywords = versionInformation.Keywords # Assemble classifiers classifiers = list(classifiers) # Translate license to classifier if not isinstance(license, License): ex = TypeError(f"Parameter 'license' is not of type 'License'.") ex.add_note(f"Got type '{getFullyQualifiedName(readmeFile)}'.") raise ex classifiers.append(license.PythonClassifier) def _naturalSorting(array: Iterable[str]) -> List[str]: """A simple natural sorting implementation.""" # See http://nedbatchelder.com/blog/200712/human_sorting.html def _toInt(text: str) -> Union[str, int]: """Try to convert a :class:`str` to :class:`int` if possible, otherwise preserve the string.""" return int(text) if text.isdigit() else text def _createKey(text: str) -> Tuple[Union[str, float], ...]: """ Split the text into a tuple of multiple :class:`str` and :class:`int` fields, so embedded numbers can be sorted by their value. """ return tuple(_toInt(part) for part in re_split(r"(\d+)", text)) sortedArray = list(array) sortedArray.sort(key=_createKey) return sortedArray pythonVersions = _naturalSorting(pythonVersions) # Translate Python versions to classifiers classifiers.append("Programming Language :: Python :: 3 :: Only") for v in pythonVersions: classifiers.append(f"Programming Language :: Python :: {v}") # Translate status to classifier try: classifiers.append(f"Development Status :: {STATUS[developmentStatus.lower()]}") except KeyError: # pragma: no cover raise ValueError(f"Unsupported development status '{developmentStatus}'.") # Assemble all package information parameters = { "name": packageName, "version": versionInformation.Version, "author": versionInformation.Author, "author_email": versionInformation.Email, "license": license.SPDXIdentifier, "description": description, "long_description": readme.Content, "long_description_content_type": readme.MimeType, "url": projectURL, "project_urls": { 'Documentation': documentationURL, 'Source Code': sourceCodeURL, 'Issue Tracker': issueTrackerCodeURL }, "packages": packages, "classifiers": classifiers, "keywords": keywords, "python_requires": f">={pythonVersions[0]}", "install_requires": requirements, } if len(extraRequirements) > 0: parameters["extras_require"] = extraRequirements if consoleScripts is not None: scripts = [] for scriptName, entryPoint in consoleScripts.items(): scripts.append(f"{scriptName} = {entryPoint}") parameters["entry_points"] = { "console_scripts": scripts } if dataFiles: parameters["package_data"] = dataFiles return parameters @export def DescribePythonPackageHostedOnGitHub( packageName: str, description: str, gitHubNamespace: str, gitHubRepository: str = None, projectURL: str = None, keywords: Iterable[str] = None, license: License = DEFAULT_LICENSE, readmeFile: Path = DEFAULT_README, requirementsFile: Path = DEFAULT_REQUIREMENTS, documentationRequirementsFile: Path = DEFAULT_DOCUMENTATION_REQUIREMENTS, unittestRequirementsFile: Path = DEFAULT_TEST_REQUIREMENTS, packagingRequirementsFile: Path = DEFAULT_PACKAGING_REQUIREMENTS, additionalRequirements: Dict[str, List[str]] = None, sourceFileWithVersion: Path = DEFAULT_VERSION_FILE, classifiers: Iterable[str] = DEFAULT_CLASSIFIERS, developmentStatus: str = "stable", pythonVersions: Sequence[str] = DEFAULT_PY_VERSIONS, consoleScripts: Dict[str, str] = None, dataFiles: Dict[str, List[str]] = None, debug: bool = False ) -> Dict[str, Any]: """ Helper function to describe a Python package when the source code is hosted on GitHub. This is a wrapper for :func:`DescribePythonPackage`, because some parameters can be simplified by knowing the GitHub namespace and repository name: issue tracker URL, source code URL, ... :param packageName: Name of the Python package. :param description: Short description of the package. The long description will be read from README file. :param gitHubNamespace: Name of the GitHub namespace (organization or user). :param gitHubRepository: Name of the GitHub repository. :param projectURL: URL to the Python project. :param keywords: A list of keywords. :param license: The package's license. (Default: ``Apache License, 2.0``, see :const:`DEFAULT_LICENSE`) :param readmeFile: The path to the README file. (Default: ``README.md``, see :const:`DEFAULT_README`) :param requirementsFile: The path to the project's requirements file. (Default: ``requirements.txt``, see :const:`DEFAULT_REQUIREMENTS`) :param documentationRequirementsFile: The path to the project's requirements file for documentation. (Default: ``doc/requirements.txt``, see :const:`DEFAULT_DOCUMENTATION_REQUIREMENTS`) :param unittestRequirementsFile: The path to the project's requirements file for unit tests. (Default: ``tests/requirements.txt``, see :const:`DEFAULT_TEST_REQUIREMENTS`) :param packagingRequirementsFile: The path to the project's requirements file for packaging. (Default: ``build/requirements.txt``, see :const:`DEFAULT_PACKAGING_REQUIREMENTS`) :param additionalRequirements: A dictionary of a lists with additional requirements. (default: None) :param sourceFileWithVersion: The path to the project's source file containing dunder variables like ``__version__``. (Default: ``__init__.py``, see :const:`DEFAULT_VERSION_FILE`) :param classifiers: A list of package classifiers. (Default: 3 classifiers, see :const:`DEFAULT_CLASSIFIERS`) :param developmentStatus: Development status of the package. (Default: stable, see :const:`STATUS` for supported status values) :param pythonVersions: A list of supported Python 3 version. (Default: all currently maintained CPython versions, see :const:`DEFAULT_PY_VERSIONS`) :param consoleScripts: A dictionary mapping command line names to entry points. (Default: None) :param dataFiles: A dictionary mapping package names to lists of additional data files. :param debug: Enable extended outputs for debugging. :returns: A dictionary suitable for :func:`setuptools.setup`. :raises ToolingException: If package 'setuptools' is not available. :raises TypeError: If parameter 'readmeFile' is not of type :class:`~pathlib.Path`. :raises FileNotFoundError: If README file doesn't exist. :raises TypeError: If parameter 'requirementsFile' is not of type :class:`~pathlib.Path`. :raises FileNotFoundError: If requirements file doesn't exist. :raises TypeError: If parameter 'documentationRequirementsFile' is not of type :class:`~pathlib.Path`. :raises TypeError: If parameter 'unittestRequirementsFile' is not of type :class:`~pathlib.Path`. :raises TypeError: If parameter 'packagingRequirementsFile' is not of type :class:`~pathlib.Path`. :raises TypeError: If parameter 'sourceFileWithVersion' is not of type :class:`~pathlib.Path`. :raises FileNotFoundError: If package file with dunder variables doesn't exist. :raises TypeError: If parameter 'license' is not of type :class:`~pyTooling.Licensing.License`. :raises ValueError: If developmentStatus uses an unsupported value. (See :const:`STATUS`) :raises ValueError: If the content type of the README file is not supported. (See :func:`loadReadmeFile`) :raises FileNotFoundError: If the README file doesn't exist. (See :func:`loadReadmeFile`) :raises FileNotFoundError: If the requirements file doesn't exist. (See :func:`loadRequirementsFile`) """ if gitHubRepository is None: # Assign GitHub repository name without '.*', if derived from Python package name. if packageName.endswith(".*"): gitHubRepository = packageName[:-2] else: gitHubRepository = packageName # Derive URLs sourceCodeURL = f"https://GitHub.com/{gitHubNamespace}/{gitHubRepository}" documentationURL = f"https://{gitHubNamespace}.GitHub.io/{gitHubRepository}" issueTrackerCodeURL = f"{sourceCodeURL}/issues" projectURL = projectURL if projectURL is not None else sourceCodeURL return DescribePythonPackage( packageName=packageName, description=description, keywords=keywords, projectURL=projectURL, sourceCodeURL=sourceCodeURL, documentationURL=documentationURL, issueTrackerCodeURL=issueTrackerCodeURL, license=license, readmeFile=readmeFile, requirementsFile=requirementsFile, documentationRequirementsFile=documentationRequirementsFile, unittestRequirementsFile=unittestRequirementsFile, packagingRequirementsFile=packagingRequirementsFile, additionalRequirements=additionalRequirements, sourceFileWithVersion=sourceFileWithVersion, classifiers=classifiers, developmentStatus=developmentStatus, pythonVersions=pythonVersions, consoleScripts=consoleScripts, dataFiles=dataFiles, debug=debug, ) pyTooling-8.11.0/pyTooling/Platform/000077500000000000000000000000001513317154500173425ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Platform/__init__.py000066400000000000000000000634261513317154500214660ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ __ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \| | __ _| |_ / _| ___ _ __ _ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_) | |/ _` | __| |_ / _ \| '__| '_ ` _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| __/| | (_| | |_| _| (_) | | | | | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\__,_|\__|_| \___/|_| |_| |_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Common platform information gathered from various sources. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from enum import Flag, auto, Enum try: from pyTooling.Decorators import export, readonly from pyTooling.Exceptions import ToolingException from pyTooling.MetaClasses import ExtendedType from pyTooling.Versioning import PythonVersion except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Platform] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from Exceptions import ToolingException from MetaClasses import ExtendedType from Versioning import PythonVersion except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Platform] Could not import directly!") raise ex __all__ = ["CurrentPlatform"] @export class PlatformException(ToolingException): """Base-exception of all exceptions raised by :mod:`pyTooling.Platform`.""" @export class UnknownPlatformException(PlatformException): """ The exception is raised by pyTooling.Platform when the platform can't be determined. For debugging purposes, a list of system properties from various APIs is added as notes to this exception to ease debugging unknown or new platforms. """ def __init__(self, *args) -> None: """ Initialize a new :class:`UnknownPlatformException` instance and add notes with further debugging information. :param args: Forward positional parameters. """ super().__init__(*args) import sys import os import platform import sysconfig self.add_note(f"os.name: {os.name}") self.add_note(f"platform.system: {platform.system()}") self.add_note(f"platform.machine: {platform.machine()}") self.add_note(f"platform.architecture: {platform.architecture()}") self.add_note(f"sys.platform: {sys.platform}") self.add_note(f"sysconfig.get_platform: {sysconfig.get_platform()}") @export class UnknownOperatingSystemException(PlatformException): """The exception is raised by pyTooling.Platform when the operating system is unknown.""" @export class PythonImplementation(Enum): """An enumeration describing the Python implementation (CPython, PyPy, ...).""" Unknown = 0 #: Unknown Python implementation CPython = 1 #: CPython (reference implementation) PyPy = 2 #: PyPy @export class Platforms(Flag): """A flag describing on which platform Python is running on and/or in which environment it's running in.""" Unknown = 0 OS_FreeBSD = auto() #: Operating System: BSD (Unix). OS_Linux = auto() #: Operating System: Linux. OS_MacOS = auto() #: Operating System: macOS. OS_Windows = auto() #: Operating System: Windows. OperatingSystem = OS_FreeBSD | OS_Linux | OS_MacOS | OS_Windows #: Mask: Any operating system. SEP_WindowsPath = auto() #: Seperator: Path element seperator (e.g. for directories). SEP_WindowsValue = auto() #: Seperator: Value seperator in variables (e.g. for paths in PATH). ENV_Native = auto() #: Environment: :term:`native`. ENV_WSL = auto() #: Environment: :term:`Windows System for Linux `. ENV_MSYS2 = auto() #: Environment: :term:`MSYS2`. ENV_Cygwin = auto() #: Environment: :term:`Cygwin`. Environment = ENV_Native | ENV_WSL | ENV_MSYS2 | ENV_Cygwin #: Mask: Any environment. CI_None = auto() #: CI: No CI environment detected. Running on host. CI_AppVeyor = auto() #: CI: AppVayor CI_GitHubActions = auto() #: CI: GitHub Actions CI_GitLabCI = auto() #: CI: GitLab CI CI_TravisCI = auto() #: CI: Travis CI ContinuousIntegration = CI_None | CI_AppVeyor | CI_GitHubActions | CI_GitLabCI | CI_TravisCI #: Mask: Any CI environment. ARCH_x86_32 = auto() #: Architecture: x86-32 (IA32). ARCH_x86_64 = auto() #: Architecture: x86-64 (AMD64). ARCH_AArch64 = auto() #: Architecture: AArch64 (arm64). Arch_x86 = ARCH_x86_32 | ARCH_x86_64 #: Mask: Any x86 architecture. Arch_Arm = ARCH_AArch64 #: Mask: Any Arm architecture. Architecture = Arch_x86 | Arch_Arm #: Mask: Any architecture. FreeBSD = OS_FreeBSD | ENV_Native #: Group: native FreeBSD on x86-64. Linux = OS_Linux | ENV_Native #: Group: native Linux on x86-64. MacOS = OS_MacOS | ENV_Native #: Group: native macOS. Windows = OS_Windows | ENV_Native | SEP_WindowsPath | SEP_WindowsValue #: Group: native Windows on x86-64. Linux_x86_64 = Linux | ARCH_x86_64 #: Group: native Linux on x86-64. Linux_AArch64 = Linux | ARCH_AArch64 #: Group: native Linux on aarch64. MacOS_Intel = MacOS | ARCH_x86_64 #: Group: native macOS on x86-64. MacOS_ARM = MacOS | ARCH_AArch64 #: Group: native macOS on aarch64. Windows_x86_64 = Windows | ARCH_x86_64 #: Group: native Windows on x86-64. Windows_AArch64 = Windows | ARCH_AArch64 #: Group: native Windows on aarch64. MSYS = auto() #: MSYS2 Runtime: MSYS. MinGW32 = auto() #: MSYS2 Runtime: :term:`MinGW32 `. MinGW64 = auto() #: MSYS2 Runtime: :term:`MinGW64 `. UCRT64 = auto() #: MSYS2 Runtime: :term:`UCRT64 `. Clang32 = auto() #: MSYS2 Runtime: Clang32. Clang64 = auto() #: MSYS2 Runtime: Clang64. MSYS2_Runtime = MSYS | MinGW32 | MinGW64 | UCRT64 | Clang32 | Clang64 #: Mask: Any MSYS2 runtime environment. Windows_MSYS2_MSYS = OS_Windows | ENV_MSYS2 | ARCH_x86_64 | MSYS #: Group: MSYS runtime running on Windows x86-64 Windows_MSYS2_MinGW32 = OS_Windows | ENV_MSYS2 | ARCH_x86_32 | MinGW32 #: Group: MinGW32 runtime running on Windows x86-64 Windows_MSYS2_MinGW64 = OS_Windows | ENV_MSYS2 | ARCH_x86_64 | MinGW64 #: Group: MinGW64 runtime running on Windows x86-64 Windows_MSYS2_UCRT64 = OS_Windows | ENV_MSYS2 | ARCH_x86_64 | UCRT64 #: Group: UCRT64 runtime running on Windows x86-64 Windows_MSYS2_Clang32 = OS_Windows | ENV_MSYS2 | ARCH_x86_32 | Clang32 #: Group: Clang32 runtime running on Windows x86-64 Windows_MSYS2_Clang64 = OS_Windows | ENV_MSYS2 | ARCH_x86_64 | Clang64 #: Group: Clang64 runtime running on Windows x86-64 Windows_Cygwin32 = OS_Windows | ENV_Cygwin | ARCH_x86_32 #: Group: 32-bit Cygwin runtime on Windows x86-64 Windows_Cygwin64 = OS_Windows | ENV_Cygwin | ARCH_x86_64 #: Group: 64-bit Cygwin runtime on Windows x86-64 @export class Platform(metaclass=ExtendedType, singleton=True, slots=True): """An instance of this class contains all gathered information available from various sources. .. seealso:: StackOverflow question: `Python: What OS am I running on? `__ """ _platform: Platforms _pythonImplementation: PythonImplementation _pythonVersion: PythonVersion def __init__(self) -> None: """ Initializes a platform by accessing multiple APIs of Python to gather all necessary information. """ import sys import os import platform import sysconfig # Discover the Python implementation pythonImplementation = platform.python_implementation() if pythonImplementation == "CPython": self._pythonImplementation = PythonImplementation.CPython elif pythonImplementation == "PyPy": self._pythonImplementation = PythonImplementation.PyPy else: # pragma: no cover self._pythonImplementation = PythonImplementation.Unknown # Discover the Python version self._pythonVersion = PythonVersion.FromSysVersionInfo() # Discover the platform self._platform = Platforms.Unknown machine = platform.machine() sys_platform = sys.platform sysconfig_platform = sysconfig.get_platform() if "APPVEYOR" in os.environ: self._platform |= Platforms.CI_AppVeyor elif "GITHUB_ACTIONS" in os.environ: self._platform |= Platforms.CI_GitHubActions elif "GITLAB_CI" in os.environ: self._platform |= Platforms.CI_GitLabCI elif "TRAVIS" in os.environ: self._platform |= Platforms.CI_TravisCI else: self._platform |= Platforms.CI_None if os.name == "nt": self._platform |= Platforms.OS_Windows if sysconfig_platform == "win32": self._platform |= Platforms.ENV_Native | Platforms.ARCH_x86_32 | Platforms.SEP_WindowsPath | Platforms.SEP_WindowsValue elif sysconfig_platform == "win-amd64": self._platform |= Platforms.ENV_Native | Platforms.ARCH_x86_64 | Platforms.SEP_WindowsPath | Platforms.SEP_WindowsValue elif sysconfig_platform == "win-arm64": self._platform |= Platforms.ENV_Native | Platforms.ARCH_AArch64 | Platforms.SEP_WindowsPath | Platforms.SEP_WindowsValue elif sysconfig_platform.startswith("mingw"): if machine == "AMD64": self._platform |= Platforms.ARCH_x86_64 else: # pragma: no cover raise UnknownPlatformException(f"Unknown architecture '{machine}' for Windows.") if sysconfig_platform == "mingw_i686_msvcrt_gnu": self._platform |= Platforms.ENV_MSYS2 | Platforms.MinGW32 elif sysconfig_platform == "mingw_x86_64_msvcrt_gnu": self._platform |= Platforms.ENV_MSYS2 | Platforms.MinGW64 elif sysconfig_platform == "mingw_x86_64_ucrt_gnu": self._platform |= Platforms.ENV_MSYS2 | Platforms.UCRT64 elif sysconfig_platform == "mingw_x86_64_ucrt_llvm": self._platform |= Platforms.ENV_MSYS2 | Platforms.Clang64 elif sysconfig_platform == "mingw_i686": # pragma: no cover self._platform |= Platforms.ENV_MSYS2 | Platforms.MinGW32 elif sysconfig_platform == "mingw_x86_64": # pragma: no cover self._platform |= Platforms.ENV_MSYS2 | Platforms.MinGW64 elif sysconfig_platform == "mingw_x86_64_ucrt": # pragma: no cover self._platform |= Platforms.ENV_MSYS2 | Platforms.UCRT64 elif sysconfig_platform == "mingw_x86_64_clang": # pragma: no cover self._platform |= Platforms.ENV_MSYS2 | Platforms.Clang64 else: # pragma: no cover raise UnknownPlatformException(f"Unknown MSYS2 architecture '{sysconfig_platform}'.") else: # pragma: no cover raise UnknownPlatformException(f"Unknown platform '{sysconfig_platform}' running on Windows.") elif os.name == "posix": if sys_platform == "linux": self._platform |= Platforms.OS_Linux | Platforms.ENV_Native if sysconfig_platform == "linux-x86_64": # native Linux x86_64; Windows 64 + WSL self._platform |= Platforms.ARCH_x86_64 elif sysconfig_platform == "linux-aarch64": # native Linux Aarch64 self._platform |= Platforms.ARCH_AArch64 else: # pragma: no cover raise UnknownPlatformException(f"Unknown architecture '{sysconfig_platform}' for a native Linux.") elif sys_platform == "darwin": self._platform |= Platforms.OS_MacOS | Platforms.ENV_Native if machine == "x86_64": self._platform |= Platforms.ARCH_x86_64 elif machine == "arm64": self._platform |= Platforms.ARCH_AArch64 else: # pragma: no cover raise UnknownPlatformException(f"Unknown architecture '{machine}' for a native macOS.") elif sys_platform == "msys": self._platform |= Platforms.OS_Windows | Platforms.ENV_MSYS2 | Platforms.MSYS if machine == "i686": self._platform |= Platforms.ARCH_x86_32 elif machine == "x86_64": self._platform |= Platforms.ARCH_x86_64 else: # pragma: no cover raise UnknownPlatformException(f"Unknown architecture '{machine}' for MSYS2-MSYS on Windows.") elif sys_platform == "cygwin": self._platform |= Platforms.OS_Windows if machine == "i686": self._platform |= Platforms.ARCH_x86_32 elif machine == "x86_64": self._platform |= Platforms.ARCH_x86_64 else: # pragma: no cover raise UnknownPlatformException(f"Unknown architecture '{machine}' for Cygwin on Windows.") elif sys_platform.startswith("freebsd"): if machine == "amd64": self._platform = Platforms.FreeBSD else: # pragma: no cover raise UnknownPlatformException(f"Unknown architecture '{machine}' for FreeBSD.") else: # pragma: no cover raise UnknownPlatformException(f"Unknown POSIX platform '{sys_platform}'.") else: # pragma: no cover raise UnknownPlatformException(f"Unknown operating system '{os.name}'.") @readonly def PythonImplementation(self) -> PythonImplementation: """ Read-only property to return the :class:`PythonImplementation` of the current interpreter. :returns: Python implementation of the current interpreter. """ return self._pythonImplementation @readonly def IsCPython(self) -> bool: """Returns true, if the Python implementation is a :term:`CPython`. :returns: ``True``, if the Python implementation is CPython. """ return self._pythonImplementation is PythonImplementation.CPython @readonly def IsPyPy(self) -> bool: """Returns true, if the Python implementation is a :term:`PyPy`. :returns: ``True``, if the Python implementation is PyPY. """ return self._pythonImplementation is PythonImplementation.PyPy @readonly def PythonVersion(self) -> PythonVersion: """ Read-only property to return the :class:`pyTooling.Versioning.PythonVersion` of the current interpreter. :returns: Python version of the current interpreter. """ return self._pythonVersion @readonly def HostOperatingSystem(self) -> Platforms: return self._platform & Platforms.OperatingSystem @readonly def IsNativePlatform(self) -> bool: """Returns true, if the platform is a :term:`native` platform. :returns: ``True``, if the platform is a native platform. """ return Platforms.ENV_Native in self._platform @readonly def IsNativeFreeBSD(self) -> bool: """Returns true, if the platform is a :term:`native` FreeBSD x86-64 platform. :returns: ``True``, if the platform is a native FreeBSD x86-64 platform. """ return Platforms.FreeBSD in self._platform @readonly def IsNativeMacOS(self) -> bool: """Returns true, if the platform is a :term:`native` macOS x86-64 platform. :returns: ``True``, if the platform is a native macOS x86-64 platform. """ return Platforms.MacOS in self._platform @readonly def IsNativeLinux(self) -> bool: """Returns true, if the platform is a :term:`native` Linux x86-64 platform. :returns: ``True``, if the platform is a native Linux x86-64 platform. """ return Platforms.Linux in self._platform @readonly def IsNativeWindows(self) -> bool: """Returns true, if the platform is a :term:`native` Windows x86-64 platform. :returns: ``True``, if the platform is a native Windows x86-64 platform. """ return Platforms.Windows in self._platform @readonly def IsMSYS2Environment(self) -> bool: """Returns true, if the platform is a :term:`MSYS2` environment on Windows. :returns: ``True``, if the platform is a MSYS2 environment on Windows. """ return Platforms.ENV_MSYS2 in self._platform @readonly def IsMSYSOnWindows(self) -> bool: """Returns true, if the platform is a MSYS runtime on Windows. :returns: ``True``, if the platform is a MSYS runtime on Windows. """ return Platforms.Windows_MSYS2_MSYS in self._platform @readonly def IsMinGW32OnWindows(self) -> bool: """Returns true, if the platform is a :term:`MinGW32 ` runtime on Windows. :returns: ``True``, if the platform is a MINGW32 runtime on Windows. """ return Platforms.Windows_MSYS2_MinGW32 in self._platform @readonly def IsMinGW64OnWindows(self) -> bool: """Returns true, if the platform is a :term:`MinGW64 ` runtime on Windows. :returns: ``True``, if the platform is a MINGW64 runtime on Windows. """ return Platforms.Windows_MSYS2_MinGW64 in self._platform @readonly def IsUCRT64OnWindows(self) -> bool: """Returns true, if the platform is a :term:`UCRT64 ` runtime on Windows. :returns: ``True``, if the platform is a UCRT64 runtime on Windows. """ return Platforms.Windows_MSYS2_UCRT64 in self._platform @readonly def IsClang32OnWindows(self) -> bool: """Returns true, if the platform is a Clang32 runtime on Windows. :returns: ``True``, if the platform is a Clang32 runtime on Windows. """ return Platforms.Windows_MSYS2_Clang32 in self._platform @readonly def IsClang64OnWindows(self) -> bool: """Returns true, if the platform is a Clang64 runtime on Windows. :returns: ``True``, if the platform is a Clang64 runtime on Windows. """ return Platforms.Windows_MSYS2_Clang64 in self._platform @readonly def IsCygwin32OnWindows(self) -> bool: """Returns true, if the platform is a 32-bit Cygwin runtime on Windows. :returns: ``True``, if the platform is a 32-bit Cygwin runtime on Windows. """ return Platforms.Windows_Cygwin32 in self._platform @readonly def IsCygwin64OnWindows(self) -> bool: """Returns true, if the platform is a 64-bit Cygwin runtime on Windows. :returns: ``True``, if the platform is a 64-bit Cygwin runtime on Windows. """ return Platforms.Windows_Cygwin64 in self._platform @readonly def IsPOSIX(self) -> bool: """ Returns true, if the platform is POSIX or POSIX-like. :returns: ``True``, if POSIX or POSIX-like. """ return Platforms.SEP_WindowsPath not in self._platform @readonly def IsCI(self) -> bool: """ Returns true, if the platform is a CI environment. :returns: ``True``, if on CI runner. """ return Platforms.CI_None not in self._platform @readonly def IsAppVeyor(self) -> bool: """ Returns true, if the platform is on AppVeyor. :returns: ``True``, if on AppVeyor. """ return Platforms.CI_AppVeyor in self._platform @readonly def IsGitHub(self) -> bool: """ Returns true, if the platform is on GitHub. :returns: ``True``, if on GitHub. """ return Platforms.CI_GitHubActions in self._platform @readonly def IsGitLab(self) -> bool: """ Returns true, if the platform is on GitLab CI. :returns: ``True``, if on GitLab CI. """ return Platforms.CI_GitLabCI in self._platform @readonly def IsTravisCI(self) -> bool: """ Returns true, if the platform is on Travis CI. :returns: ``True``, if on Travis CI. """ return Platforms.CI_TravisCI in self._platform @readonly def PathSeperator(self) -> str: """ Returns the path element separation character (e.g. for directories). * POSIX-like: ``/`` * Windows: ``\\`` :returns: Path separation character. """ if Platforms.SEP_WindowsPath in self._platform: return "\\" else: return "/" @readonly def ValueSeperator(self) -> str: """ Returns the value separation character (e.g. for paths in PATH). * POSIX-like: ``:`` * Windows: ``;`` :returns: Value separation character. """ if Platforms.SEP_WindowsValue in self._platform: return ";" else: return ":" @readonly def ExecutableExtension(self) -> str: """ Returns the file extension for an executable. * FreeBSD: ``""`` (empty string) * Linux: ``""`` (empty string) * macOS: ``""`` (empty string) * Windows: ``"exe"`` :returns: File extension of an executable. :raises UnknownOperatingSystemException: If the operating system is unknown. """ if Platforms.OS_FreeBSD in self._platform: return "" elif Platforms.OS_Linux in self._platform: return "" elif Platforms.OS_MacOS in self._platform: return "" elif Platforms.OS_Windows in self._platform: return "exe" else: # pragma: no cover raise UnknownOperatingSystemException("Unknown operating system.") @readonly def StaticLibraryExtension(self) -> str: """ Returns the file extension for a static library. * FreeBSD: ``"a"`` * Linux: ``"a"`` * macOS: ``"lib"`` * Windows: ``"lib"`` :returns: File extension of a static library. :raises UnknownOperatingSystemException: If the operating system is unknown. """ if Platforms.OS_FreeBSD in self._platform: return "a" elif Platforms.OS_Linux in self._platform: return "a" elif Platforms.OS_MacOS in self._platform: return "a" elif Platforms.OS_Windows in self._platform: return "lib" else: # pragma: no cover raise UnknownOperatingSystemException("Unknown operating system.") @readonly def DynamicLibraryExtension(self) -> str: """ Returns the file extension for a dynamic/shared library. * FreeBSD: ``"so"`` * Linux: ``"so"`` * macOS: ``"dylib"`` * Windows: ``"dll"`` :returns: File extension of a dynamic library. :raises UnknownOperatingSystemException: If the operating system is unknown. """ if Platforms.OS_FreeBSD in self._platform: return "so" elif Platforms.OS_Linux in self._platform: return "so" elif Platforms.OS_MacOS in self._platform: return "dylib" elif Platforms.OS_Windows in self._platform: return "dll" else: # pragma: no cover raise UnknownOperatingSystemException("Unknown operating system.") def __repr__(self) -> str: """ Returns the platform's string representation. :returns: The string representation of the current platform. """ return str(self._platform) def __str__(self) -> str: """ Returns the platform's string equivalent. :returns: The string equivalent of the platform. """ runtime = "" if Platforms.OS_FreeBSD in self._platform: platform = "FreeBSD" elif Platforms.OS_MacOS in self._platform: platform = "macOS" elif Platforms.OS_Linux in self._platform: platform = "Linux" elif Platforms.OS_Windows in self._platform: platform = "Windows" else: platform = "plat:dec-err" if Platforms.ENV_Native in self._platform: environment = "" elif Platforms.ENV_WSL in self._platform: environment = "+WSL" elif Platforms.ENV_MSYS2 in self._platform: environment = "+MSYS2" if Platforms.MSYS in self._platform: runtime = " - MSYS" elif Platforms.MinGW32 in self._platform: runtime = " - MinGW32" elif Platforms.MinGW64 in self._platform: runtime = " - MinGW64" elif Platforms.UCRT64 in self._platform: runtime = " - UCRT64" elif Platforms.Clang32 in self._platform: runtime = " - Clang32" elif Platforms.Clang64 in self._platform: runtime = " - Clang64" else: runtime = "rt:dec-err" elif Platforms.ENV_Cygwin in self._platform: environment = "+Cygwin" else: environment = "env:dec-err" if Platforms.ARCH_x86_32 in self._platform: architecture = "x86-32" elif Platforms.ARCH_x86_64 in self._platform: architecture = "x86-64" elif Platforms.ARCH_AArch64 in self._platform: architecture = "aarch64" else: architecture = "arch:dec-err" return f"{platform}{environment} ({architecture}){runtime}" CurrentPlatform = Platform() #: Gathered information for the current platform. pyTooling-8.11.0/pyTooling/StateMachine/000077500000000000000000000000001513317154500201235ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/StateMachine/__init__.py000066400000000000000000000136501513317154500222410ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ __ __ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|| |_ __ _| |_ ___| \/ | __ _ ___| |__ (_)_ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | \___ \| __/ _` | __/ _ \ |\/| |/ _` |/ __| '_ \| | '_ \ / _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_ ___) | || (_| | || __/ | | | (_| | (__| | | | | | | | __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \__\__,_|\__\___|_| |_|\__,_|\___|_| |_|_|_| |_|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ This packages provides a data structure to describe statemachines. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from typing import List try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.StateMachine] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, mixin except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.StateMachine] Could not import directly!") raise ex @export class Base(metaclass=ExtendedType, slots=True): pass @export class Transition(Base): """ Represents a transition (edge) in a statemachine diagram (directed graph). """ _source: "State" #: Source state. _destination: "State" #: Destination state. def __init__(self, source: "State", destination: "State") -> None: """ Initializes a transition. :param source: Source state of a transition. :param destination: Destination state of a transition. """ self._source = source self._destination = destination @export class State(Base): """ Represents a state (node/vertex) in a statemachine diagram (directed graph). """ _inboundTransitions: List[Transition] #: List of inbound transitions. _outboundTransitions: List[Transition] #: List of outbound transitions. def __init__(self) -> None: """ Initializes a state. """ self._inboundTransitions = [] self._outboundTransitions = [] @export class StateMachine(Base): """ Represents a statemachine (graph) in a statemachine diagram (directed graph). """ _states: List[State] _initialState: State def __init__(self, initialState: State) -> None: """ Initializes a (finite) state machine (FSM). :param initialState: The initialize state of the FSM. """ self._states = [] self._initialState = initialState def AddState(self, state: State) -> None: """ Add a state to the state machine. :param state: State to add. """ if state not in self._states: # TODO: use a set to check for double added states? self._states.append(state) else: raise ValueError(f"State '{state}' was already added to this statemachine.") @readonly def States(self) -> List[State]: """ Read-only property to access the list of states. :returns: List of states. """ return self._states pyTooling-8.11.0/pyTooling/Stopwatch/000077500000000000000000000000001513317154500175325ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Stopwatch/__init__.py000066400000000000000000000516541513317154500216560ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|| |_ ___ _ ____ ____ _| |_ ___| |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | \___ \| __/ _ \| '_ \ \ /\ / / _` | __/ __| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_ ___) | || (_) | |_) \ V V / (_| | || (__| | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \__\___/| .__/ \_/\_/ \__,_|\__\___|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ A stopwatch to measure execution times. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from datetime import datetime from time import perf_counter_ns from types import TracebackType from typing import List, Optional as Nullable, Iterator, Tuple, Type, Self try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import SlottedObject from pyTooling.Exceptions import ToolingException from pyTooling.Platform import CurrentPlatform except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Stopwatch] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import SlottedObject from Exceptions import ToolingException from Platform import CurrentPlatform except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Stopwatch] Could not import directly!") raise ex @export class StopwatchException(ToolingException): """This exception is caused by wrong usage of the stopwatch.""" @export class ExcludeContextManager: """ A stopwatch context manager for excluding certain time spans from measurement. While a normal stopwatch's embedded context manager (re)starts the stopwatch on every *enter* event and pauses the stopwatch on every *exit* event, this context manager pauses on *enter* events and restarts on every *exit* event. """ _stopwatch: "Stopwatch" #: Reference to the stopwatch. def __init__(self, stopwatch: "Stopwatch") -> None: """ Initializes an excluding context manager. :param stopwatch: Reference to the stopwatch. """ self._stopwatch = stopwatch def __enter__(self) -> Self: """ Enter the context and pause the stopwatch. :returns: Excluding stopwatch context manager instance. """ self._stopwatch.Pause() return self def __exit__( self, exc_type: Nullable[Type[BaseException]] = None, exc_val: Nullable[BaseException] = None, exc_tb: Nullable[TracebackType] = None ) -> Nullable[bool]: """ Exit the context and restart stopwatch. :param exc_type: Exception type :param exc_val: Exception instance :param exc_tb: Exception's traceback. :returns: ``None`` """ self._stopwatch.Resume() @export class Stopwatch(SlottedObject): """ The stopwatch implements a solution to measure and collect timings. The time measurement can be started, paused, resumed and stopped. More over, split times can be taken too. The measurement is based on :func:`time.perf_counter_ns`. Additionally, starting and stopping is preserved as absolute time via :meth:`datetime.datetime.now`. Every split time taken is a time delta to the previous operation. These are preserved in an internal sequence of splits. This sequence includes time deltas of activity and inactivity. Thus, a running stopwatch can be split as well as a paused stopwatch. The stopwatch can also be used in a :ref:`with-statement `, because it implements the :ref:`context manager protocol `. """ _name: Nullable[str] _preferPause: bool _beginTime: Nullable[datetime] _endTime: Nullable[datetime] _startTime: Nullable[int] _resumeTime: Nullable[int] _pauseTime: Nullable[int] _stopTime: Nullable[int] _totalTime: Nullable[int] _splits: List[Tuple[float, bool]] _excludeContextManager: ExcludeContextManager def __init__(self, name: str = None, started: bool = False, preferPause: bool = False) -> None: """ Initializes the fields of the stopwatch. If parameter ``started`` is set to true, the stopwatch will immediately start. :param name: Optional name of the stopwatch. :param preferPause: Optional setting, if __exit__(...) in a contex should prefer pause or stop behavior. :param started: Optional flag, if the stopwatch should be started immediately. """ self._name = name self._preferPause = preferPause self._endTime = None self._pauseTime = None self._stopTime = None self._totalTime = None self._splits = [] self._excludeContextManager = None if started is False: self._beginTime = None self._startTime = None self._resumeTime = None else: self._beginTime = datetime.now() self._resumeTime = self._startTime = perf_counter_ns() def Start(self) -> None: """ Start the stopwatch. A stopwatch can only be started once. There is no restart or reset operation provided. :raises StopwatchException: If stopwatch was already started. :raises StopwatchException: If stopwatch was already started and stopped. """ if self._startTime is not None: raise StopwatchException("Stopwatch was already started.") if self._stopTime is not None: raise StopwatchException("Stopwatch was already used (started and stopped).") self._beginTime = datetime.now() self._resumeTime = self._startTime = perf_counter_ns() def Split(self) -> float: """ Take a split time and return the time delta to the previous stopwatch operation. The stopwatch needs to be running to take a split time. See property :data:`IsRunning` to check if the stopwatch is running and the split operation is possible. |br| Depending on the previous operation, the time delta will be: * the duration from start operation to the first split. * the duration from last resume to this split. :returns: Duration in seconds since last stopwatch operation :raises StopwatchException: If stopwatch was not started or resumed. """ pauseTime = perf_counter_ns() if self._resumeTime is None: raise StopwatchException("Stopwatch was not started or resumed.") diff = (pauseTime - self._resumeTime) / 1e9 self._splits.append((diff, True)) self._resumeTime = pauseTime return diff def Pause(self) -> float: """ Pause the stopwatch and return the time delta to the previous stopwatch operation. The stopwatch needs to be running to pause it. See property :data:`IsRunning` to check if the stopwatch is running and the pause operation is possible. |br| Depending on the previous operation, the time delta will be: * the duration from start operation to the first pause. * the duration from last resume to this pause. :returns: Duration in seconds since last stopwatch operation :raises StopwatchException: If stopwatch was not started or resumed. """ self._pauseTime = perf_counter_ns() if self._resumeTime is None: raise StopwatchException("Stopwatch was not started or resumed.") diff = (self._pauseTime - self._resumeTime) / 1e9 self._splits.append((diff, True)) self._resumeTime = None return diff def Resume(self) -> float: """ Resume the stopwatch and return the time delta to the previous pause operation. The stopwatch needs to be paused to resume it. See property :data:`IsPaused` to check if the stopwatch is paused and the resume operation is possible. |br| The time delta will be the duration from last pause to this resume. :returns: Duration in seconds since last pause operation :raises StopwatchException: If stopwatch was not paused. """ self._resumeTime = perf_counter_ns() if self._pauseTime is None: raise StopwatchException("Stopwatch was not paused.") diff = (self._resumeTime - self._pauseTime) / 1e9 self._splits.append((diff, False)) self._pauseTime = None return diff def Stop(self) -> float: """ Stop the stopwatch and return the time delta to the previous stopwatch operation. The stopwatch needs to be started to stop it. See property :data:`IsStarted` to check if the stopwatch was started and the stop operation is possible. |br| Depending on the previous operation, the time delta will be: * the duration from start operation to the stop operation. * the duration from last resume to the stop operation. :returns: Duration in seconds since last stopwatch operation :raises StopwatchException: If stopwatch was not started. :raises StopwatchException: If stopwatch was already stopped. """ self._stopTime = perf_counter_ns() self._endTime = datetime.now() if self._startTime is None: raise StopwatchException("Stopwatch was never started.") if self._totalTime is not None: raise StopwatchException("Stopwatch was already stopped.") if len(self._splits) == 0: # was never paused diff = (self._stopTime - self._startTime) / 1e9 elif self._resumeTime is None: # is paused diff = (self._stopTime - self._pauseTime) / 1e9 self._splits.append((diff, False)) else: # is running diff = (self._stopTime - self._resumeTime) / 1e9 self._splits.append((diff, True)) self._pauseTime = None self._resumeTime = None self._totalTime = self._stopTime - self._startTime # FIXME: why is this unused? beginEndDiff = self._endTime - self._beginTime return diff @readonly def Name(self) -> Nullable[str]: """ Read-only property returning the name of the stopwatch. :return: Name of the stopwatch. """ return self._name @readonly def IsStarted(self) -> bool: """ Read-only property returning the IsStarted state of the stopwatch. :return: True, if stopwatch was started. """ return self._startTime is not None and self._stopTime is None @readonly def IsRunning(self) -> bool: """ Read-only property returning the IsRunning state of the stopwatch. :return: True, if stopwatch was started and is currently not paused. """ return self._startTime is not None and self._resumeTime is not None @readonly def IsPaused(self) -> bool: """ Read-only property returning the IsPaused state of the stopwatch. :return: True, if stopwatch was started and is currently paused. """ return self._startTime is not None and self._pauseTime is not None @readonly def IsStopped(self) -> bool: """ Read-only property returning the IsStopped state of the stopwatch. :return: True, if stopwatch was stopped. """ return self._stopTime is not None @readonly def StartTime(self) -> Nullable[datetime]: """ Read-only property returning the absolute time when the stopwatch was started. :return: The time when the stopwatch was started, otherwise None. """ return self._beginTime @readonly def StopTime(self) -> Nullable[datetime]: """ Read-only property returning the absolute time when the stopwatch was stopped. :return: The time when the stopwatch was stopped, otherwise None. """ return self._endTime @readonly def HasSplitTimes(self) -> bool: """ Read-only property checking if split times have been taken. :return: True, if split times have been taken. """ return len(self._splits) > 1 @readonly def SplitCount(self) -> int: """ Read-only property returning the number of split times. :return: Number of split times. """ return len(self._splits) @readonly def ActiveCount(self) -> int: """ Read-only property returning the number of active split times. :return: Number of active split times. .. warning:: This won't include all activities, unless the stopwatch got stopped. """ if self._startTime is None: return 0 return len(list(t for t, a in self._splits if a is True)) @readonly def InactiveCount(self) -> int: """ Read-only property returning the number of active split times. :return: Number of active split times. .. warning:: This won't include all inactivities, unless the stopwatch got stopped. """ if self._startTime is None: return 0 return len(list(t for t, a in self._splits if a is False)) @readonly def Activity(self) -> float: """ Read-only property returning the duration of all active split times. If the stopwatch is currently running, the duration since start or last resume operation will be included. :return: Duration of all active split times in seconds. If the stopwatch was never started, the return value will be 0.0. """ if self._startTime is None: return 0.0 currentDiff = 0.0 if self._resumeTime is None else ((perf_counter_ns() - self._resumeTime) / 1e9) return sum(t for t, a in self._splits if a is True) + currentDiff @readonly def Inactivity(self) -> float: """ Read-only property returning the duration of all inactive split times. If the stopwatch is currently paused, the duration since last pause operation will be included. :return: Duration of all inactive split times in seconds. If the stopwatch was never started, the return value will be 0.0. """ if self._startTime is None: return 0.0 currentDiff = 0.0 if self._pauseTime is None else ((perf_counter_ns() - self._pauseTime) / 1e9) return sum(t for t, a in self._splits if a is False) + currentDiff @readonly def Duration(self) -> float: """ Read-only property returning the duration from start operation to stop operation. If the stopwatch is not yet stopped, the duration from start to now is returned. :return: Duration since stopwatch was started in seconds. If the stopwatch was never started, the return value will be 0.0. """ if self._startTime is None: return 0.0 return ((perf_counter_ns() - self._startTime) if self._stopTime is None else self._totalTime) / 1e9 @readonly def Exclude(self) -> ExcludeContextManager: """ Return an *exclude* context manager for the stopwatch instance. :returns: An excluding context manager. """ if self._excludeContextManager is None: excludeContextManager = ExcludeContextManager(self) self._excludeContextManager = excludeContextManager return excludeContextManager def __enter__(self) -> Self: """ Implementation of the :ref:`context manager protocol's ` ``__enter__(...)`` method. An unstarted stopwatch will be started. A paused stopwatch will be resumed. :return: The stopwatch itself. """ if self._startTime is None: # start stopwatch self._beginTime = datetime.now() self._resumeTime = self._startTime = perf_counter_ns() elif self._pauseTime is not None: # resume after pause self._resumeTime = perf_counter_ns() diff = (self._resumeTime - self._pauseTime) / 1e9 self._splits.append((diff, False)) self._pauseTime = None elif self._resumeTime is not None: # is running? raise StopwatchException("Stopwatch is currently running and can not be started/resumed again.") elif self._stopTime is not None: # is stopped? raise StopwatchException(f"Stopwatch was already stopped.") else: raise StopwatchException(f"Internal error.") return self def __exit__( self, exc_type: Nullable[Type[BaseException]] = None, exc_val: Nullable[BaseException] = None, exc_tb: Nullable[TracebackType] = None ) -> Nullable[bool]: """ Implementation of the :ref:`context manager protocol's ` ``__exit__(...)`` method. A running stopwatch will be paused or stopped depending on the configured ``preferPause`` behavior. :param exc_type: Exception type, otherwise None. :param exc_val: Exception object, otherwise None. :param exc_tb: Exception's traceback, otherwise None. :returns: True, if exceptions should be suppressed. """ if self._startTime is None: # never started? raise StopwatchException("Stopwatch was never started.") elif self._stopTime is not None: raise StopwatchException("Stopwatch was already stopped.") elif self._resumeTime is not None: # pause or stop if self._preferPause: self._pauseTime = perf_counter_ns() diff = (self._pauseTime - self._resumeTime) / 1e9 self._splits.append((diff, True)) self._resumeTime = None else: self._stopTime = perf_counter_ns() self._endTime = datetime.now() diff = (self._stopTime - self._resumeTime) / 1e9 self._splits.append((diff, True)) self._pauseTime = None self._resumeTime = None self._totalTime = self._stopTime - self._startTime else: raise StopwatchException("Stopwatch was not resumed.") def __len__(self) -> int: """ Implementation of ``len(...)`` to return the number of split times. :return: Number of split times. """ return len(self._splits) def __getitem__(self, index: int) -> Tuple[float, bool]: """ Implementation of ``split = object[i]`` to return the i-th split time. :param index: Index to access the i-th split time. :return: i-th split time as a tuple of: |br| (1) delta time to the previous stopwatch operation and |br| (2) a boolean indicating if the split was an activity (true) or inactivity (false). :raises KeyError: If index *i* doesn't exist. """ return self._splits[index] def __iter__(self) -> Iterator[Tuple[float, bool]]: """ Return an iterator of tuples to iterate all split times. If the stopwatch is not stopped yet, the last split won't be included. :return: Iterator of split time tuples of: |br| (1) delta time to the previous stopwatch operation and |br| (2) a boolean indicating if the split was an activity (true) or inactivity (false). """ return self._splits.__iter__() def __str__(self) -> str: """ Returns the stopwatch's state and its measured time span. :returns: The string equivalent of the stopwatch. """ name = f" {self._name}" if self._name is not None else "" if self.IsStopped: return f"Stopwatch{name} (stopped): {self._beginTime} -> {self._endTime}: {self._totalTime}" elif self.IsRunning: return f"Stopwatch{name} (running): {self._beginTime} -> now: {self.Duration}" elif self.IsPaused: return f"Stopwatch{name} (paused): {self._beginTime} -> now: {self.Duration}" else: return f"Stopwatch{name}: not started" pyTooling-8.11.0/pyTooling/TerminalUI/000077500000000000000000000000001513317154500175675ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/TerminalUI/__init__.py000066400000000000000000001212771513317154500217120ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ _ _ _ ___ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _|__ _ __ _ __ ___ (_)_ __ __ _| | | | |_ _| # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | | | || | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | __/ | | | | | | | | | | | (_| | | |_| || | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|\___/|___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """A set of helpers to implement a text user interface (TUI) in a terminal.""" from enum import Enum, unique from io import TextIOWrapper from sys import stdin, stdout, stderr from textwrap import dedent from typing import NoReturn, Tuple, Any, List, Optional as Nullable, Dict, Callable, ClassVar try: from colorama import Fore as Foreground except ImportError as ex: # pragma: no cover raise Exception(f"Optional dependency 'colorama' not installed. Either install pyTooling with extra dependencies 'pyTooling[terminal]' or install 'colorama' directly.") from ex try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType, mixin from pyTooling.Exceptions import PlatformNotSupportedException, ExceptionBase from pyTooling.Common import lastItem from pyTooling.Platform import Platform except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.TerminalUI] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, mixin from Exceptions import PlatformNotSupportedException, ExceptionBase from Common import lastItem from Platform import Platform except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.TerminalUI] Could not import directly!") raise ex @export class TerminalBaseApplication(metaclass=ExtendedType, slots=True, singleton=True): """ The class offers a basic terminal application base-class. It offers basic colored output via `colorama `__ as well as retrieving the terminal's width. """ NOT_IMPLEMENTED_EXCEPTION_EXIT_CODE: ClassVar[int] = 240 #: Return code, if unimplemented methods or code sections were called. UNHANDLED_EXCEPTION_EXIT_CODE: ClassVar[int] = 241 #: Return code, if an unhandled exception reached the topmost exception handler. PYTHON_VERSION_CHECK_FAILED_EXIT_CODE: ClassVar[int] = 254 #: Return code, if version check was not successful. FATAL_EXIT_CODE: ClassVar[int] = 255 #: Return code for fatal exits. ISSUE_TRACKER_URL: ClassVar[str] = None #: URL to the issue tracker for reporting bugs. INDENT: ClassVar[str] = " " #: Indentation. Default: ``" "`` (2 spaces) try: from colorama import Fore as Foreground Foreground = { "RED": Foreground.LIGHTRED_EX, "DARK_RED": Foreground.RED, "GREEN": Foreground.LIGHTGREEN_EX, "DARK_GREEN": Foreground.GREEN, "YELLOW": Foreground.LIGHTYELLOW_EX, "DARK_YELLOW": Foreground.YELLOW, "MAGENTA": Foreground.LIGHTMAGENTA_EX, "BLUE": Foreground.LIGHTBLUE_EX, "DARK_BLUE": Foreground.BLUE, "CYAN": Foreground.LIGHTCYAN_EX, "DARK_CYAN": Foreground.CYAN, "GRAY": Foreground.WHITE, "DARK_GRAY": Foreground.LIGHTBLACK_EX, "WHITE": Foreground.LIGHTWHITE_EX, "NOCOLOR": Foreground.RESET, "HEADLINE": Foreground.LIGHTMAGENTA_EX, "ERROR": Foreground.LIGHTRED_EX, "WARNING": Foreground.LIGHTYELLOW_EX } #: Terminal colors except ImportError: # pragma: no cover Foreground = { "RED": "", "DARK_RED": "", "GREEN": "", "DARK_GREEN": "", "YELLOW": "", "DARK_YELLOW": "", "MAGENTA": "", "BLUE": "", "DARK_BLUE": "", "CYAN": "", "DARK_CYAN": "", "GRAY": "", "DARK_GRAY": "", "WHITE": "", "NOCOLOR": "", "HEADLINE": "", "ERROR": "", "WARNING": "" } #: Terminal colors _stdin: TextIOWrapper #: STDIN _stdout: TextIOWrapper #: STDOUT _stderr: TextIOWrapper #: STDERR _width: int #: Terminal width in characters _height: int #: Terminal height in characters def __init__(self) -> None: """ Initialize a terminal. If the Python package `colorama `_ [#f_colorama]_ is available, then initialize it for colored outputs. .. [#f_colorama] Colorama on Github: https://GitHub.com/tartley/colorama """ self._stdin = stdin self._stdout = stdout self._stderr = stderr if stdout.isatty(): self.InitializeColors() else: self.UninitializeColors() self._width, self._height = self.GetTerminalSize() def InitializeColors(self) -> bool: """ Initialize the terminal for color support by `colorama `__. :returns: True, if 'colorama' package could be imported and initialized. """ try: from colorama import init init() return True except ImportError: # pragma: no cover return False def UninitializeColors(self) -> bool: """ Uninitialize the terminal for color support by `colorama `__. :returns: True, if 'colorama' package could be imported and uninitialized. """ try: from colorama import deinit deinit() return True except ImportError: # pragma: no cover return False @readonly def Width(self) -> int: """ Read-only property to access the terminal's width. :returns: The terminal window's width in characters. """ return self._width @readonly def Height(self) -> int: """ Read-only property to access the terminal's height. :returns: The terminal window's height in characters. """ return self._height @staticmethod def GetTerminalSize() -> Tuple[int, int]: """ Returns the terminal size as tuple (width, height) for Windows, macOS (Darwin), Linux, cygwin (Windows), MinGW32/64 (Windows). :returns: A tuple containing width and height of the terminal's size in characters. :raises PlatformNotSupportedException: When a platform is not yet supported. """ platform = Platform() if platform.IsNativeWindows: size = TerminalBaseApplication.__GetTerminalSizeOnWindows() elif (platform.IsNativeLinux or platform.IsNativeFreeBSD or platform.IsNativeMacOS or platform.IsMinGW32OnWindows or platform.IsMinGW64OnWindows or platform.IsUCRT64OnWindows or platform.IsCygwin32OnWindows or platform.IsClang64OnWindows): size = TerminalBaseApplication.__GetTerminalSizeOnLinux() else: # pragma: no cover raise PlatformNotSupportedException(f"Platform '{platform}' not yet supported.") if size is None: # pragma: no cover size = (80, 25) # default size return size @staticmethod def __GetTerminalSizeOnWindows() -> Tuple[int, int]: """ Returns the current terminal window's size for Windows. ``kernel32.dll:GetConsoleScreenBufferInfo()`` is used to retrieve the information. :returns: A tuple containing width and height of the terminal's size in characters. """ try: from ctypes import windll, create_string_buffer from struct import unpack as struct_unpack hStdError = windll.kernel32.GetStdHandle(-12) # stderr handle = -12 stringBuffer = create_string_buffer(22) result = windll.kernel32.GetConsoleScreenBufferInfo(hStdError, stringBuffer) if result: bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy = struct_unpack("hhhhHhhhhhh", stringBuffer.raw) width = right - left + 1 height = bottom - top + 1 return (width, height) except ImportError: pass return None # return Terminal.__GetTerminalSizeWithTPut() # @staticmethod # def __GetTerminalSizeWithTPut() -> Tuple[int, int]: # """ # Returns the current terminal window's size for Windows. # # ``tput`` is used to retrieve the information. # # :returns: A tuple containing width and height of the terminal's size in characters. # """ # from subprocess import check_output # # try: # width = int(check_output(("tput", "cols"))) # height = int(check_output(("tput", "lines"))) # return (width, height) # except: # pass @staticmethod def __GetTerminalSizeOnLinux() -> Nullable[Tuple[int, int]]: # Python 3.10: Use bitwise-or for union type: | None: """ Returns the current terminal window's size for Linux. ``ioctl(TIOCGWINSZ)`` is used to retrieve the information. As a fallback, environment variables ``COLUMNS`` and ``LINES`` are checked. :returns: A tuple containing width and height of the terminal's size in characters. """ import os def ioctl_GWINSZ(fd) -> Nullable[Tuple[int, int]]: # Python 3.10: Use bitwise-or for union type: | None: """GetWindowSize of file descriptor.""" try: from fcntl import ioctl as fcntl_ioctl from struct import unpack as struct_unpack from termios import TIOCGWINSZ except ImportError: return None try: struct = struct_unpack('hh', fcntl_ioctl(fd, TIOCGWINSZ, '1234')) except OSError: return None try: return (int(struct[1]), int(struct[0])) except TypeError: return None # STDIN, STDOUT, STDERR for fd in range(3): size = ioctl_GWINSZ(fd) if size is not None: return size else: try: fd = os.open(os.ctermid(), os.O_RDONLY) size = ioctl_GWINSZ(fd) os.close(fd) return size except (OSError, AttributeError): pass try: columns = int(os.getenv("COLUMNS")) lines = int(os.getenv("LINES")) return (columns, lines) except TypeError: pass return None def WriteToStdOut(self, message: str) -> int: """ Low-level method for writing to ``STDOUT``. :param message: Message to write to ``STDOUT``. :return: Number of written characters. """ return self._stdout.write(message) def WriteLineToStdOut(self, message: str, end: str = "\n") -> int: """ Low-level method for writing to ``STDOUT``. :param message: Message to write to ``STDOUT``. :param end: Use newline character. Default: ``\\n``. :return: Number of written characters. """ return self._stdout.write(message + end) def WriteToStdErr(self, message: str) -> int: """ Low-level method for writing to ``STDERR``. :param message: Message to write to ``STDERR``. :return: Number of written characters. """ return self._stderr.write(message) def WriteLineToStdErr(self, message: str, end: str = "\n") -> int: """ Low-level method for writing to ``STDERR``. :param message: Message to write to ``STDERR``. :param end: Use newline character. Default: ``\\n``. :returns: Number of written characters. """ return self._stderr.write(message + end) def FatalExit(self, returnCode: int = 0) -> NoReturn: """ Exit the terminal application by uninitializing color support and returning a fatal Exit code. :param returnCode: Return code for application exit. """ self.Exit(self.FATAL_EXIT_CODE if returnCode == 0 else returnCode) def Exit(self, returnCode: int = 0) -> NoReturn: """ Exit the terminal application by uninitializing color support and returning an Exit code. :param returnCode: Return code for application exit. """ self.UninitializeColors() exit(returnCode) def CheckPythonVersion(self, version: Tuple[int, ...]) -> None: """ Check if the used Python interpreter fulfills the minimum version requirements. """ from sys import version_info as info if info < version: self.InitializeColors() self.WriteLineToStdErr(dedent(f"""\ {{RED}}[ERROR]{{NOCOLOR}} Used Python interpreter ({info.major}.{info.minor}.{info.micro}-{info.releaselevel}) is to old. {{indent}}{{YELLOW}}Minimal required Python version is {version[0]}.{version[1]}.{version[2]}{{NOCOLOR}}\ """).format(indent=self.INDENT, **self.Foreground)) self.Exit(self.PYTHON_VERSION_CHECK_FAILED_EXIT_CODE) def PrintException(self, ex: Exception) -> NoReturn: """ Prints an exception of type :exc:`Exception` and its traceback. If the exception as a nested action, the cause is printed as well. If ``ISSUE_TRACKER_URL`` is configured, a URL to the issue tracker is added. """ from traceback import format_tb, walk_tb frame, sourceLine = lastItem(walk_tb(ex.__traceback__)) filename = frame.f_code.co_filename funcName = frame.f_code.co_name message = f"{{RED}}[FATAL] An unknown or unhandled exception reached the topmost exception handler!{{NOCOLOR}}\n" message += f"{{indent}}{{YELLOW}}Exception type:{{NOCOLOR}} {{DARK_RED}}{ex.__class__.__name__}{{NOCOLOR}}\n" message += f"{{indent}}{{YELLOW}}Exception message:{{NOCOLOR}} {{RED}}{ex!s}{{NOCOLOR}}\n" if hasattr(ex, "__notes__") and len(ex.__notes__) > 0: note = next(iterator := iter(ex.__notes__)) message += f"{{indent}}{{YELLOW}}Notes:{{NOCOLOR}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" for note in iterator: message += f"{{indent}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" message += f"{{indent}}{{YELLOW}}Caused in:{{NOCOLOR}} {funcName}(...) in file '{filename}' at line {sourceLine}\n" if (ex2 := ex.__cause__) is not None: message += f"{{indent2}}{{DARK_YELLOW}}Caused by ex. type:{{NOCOLOR}} {{DARK_RED}}{ex2.__class__.__name__}{{NOCOLOR}}\n" message += f"{{indent2}}{{DARK_YELLOW}}Caused by message:{{NOCOLOR}} {ex2!s}{{NOCOLOR}}\n" if hasattr(ex2, "__notes__") and len(ex2.__notes__) > 0: note = next(iterator := iter(ex2.__notes__)) message += f"{{indent2}}{{DARK_YELLOW}}Notes:{{NOCOLOR}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" for note in iterator: message += f"{{indent2}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" message += f"{{indent}}{{RED}}{'-' * 120}{{NOCOLOR}}\n" for line in format_tb(ex.__traceback__): message += f"{line.replace('{', '{{').replace('}', '}}')}" message += f"{{indent}}{{RED}}{'-' * 120}{{NOCOLOR}}" if self.ISSUE_TRACKER_URL is not None: message += f"\n{{indent}}{{DARK_CYAN}}Please report this bug at GitHub: {self.ISSUE_TRACKER_URL}{{NOCOLOR}}\n" message += f"{{indent}}{{RED}}{'-' * 120}{{NOCOLOR}}" self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT*2, **self.Foreground)) self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE) def PrintNotImplementedError(self, ex: NotImplementedError) -> NoReturn: """Prints a not-implemented exception of type :exc:`NotImplementedError`.""" from traceback import walk_tb frame, sourceLine = lastItem(walk_tb(ex.__traceback__)) filename = frame.f_code.co_filename funcName = frame.f_code.co_name message = f"{{RED}}[NOT IMPLEMENTED] An unimplemented function or abstract method was called!{{NOCOLOR}}\n" message += f"{{indent}}{{YELLOW}}Function or method:{{NOCOLOR}} {{DARK_RED}}{funcName}(...){{NOCOLOR}}\n" message += f"{{indent}}{{YELLOW}}Exception message:{{NOCOLOR}} {{RED}}{ex!s}{{NOCOLOR}}\n" if hasattr(ex, "__notes__") and len(ex.__notes__) > 0: note = next(iterator := iter(ex.__notes__)) message += f"{{indent}}{{YELLOW}}Notes:{{NOCOLOR}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" for note in iterator: message += f"{{indent}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" message += f"{{indent}}{{YELLOW}}Caused in:{{NOCOLOR}} {funcName}(...) in file '{filename}' at line {sourceLine}\n" if self.ISSUE_TRACKER_URL is not None: message += f"\n{{indent}}{{DARK_CYAN}}Please report this bug at GitHub: {self.ISSUE_TRACKER_URL}{{NOCOLOR}}\n" message += f"{{indent}}{{RED}}{'-' * 120}{{NOCOLOR}}" self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT * 2, **self.Foreground)) self.Exit(self.NOT_IMPLEMENTED_EXCEPTION_EXIT_CODE) def PrintExceptionBase(self, ex: Exception) -> NoReturn: """ Prints an exception of type :exc:`ExceptionBase` and its traceback. If the exception as a nested action, the cause is printed as well. If ``ISSUE_TRACKER_URL`` is configured, a URL to the issue tracker is added. """ from traceback import print_tb, walk_tb frame, sourceLine = lastItem(walk_tb(ex.__traceback__)) filename = frame.f_code.co_filename funcName = frame.f_code.co_name self.WriteLineToStdErr(dedent(f"""\ {{RED}}[FATAL] A known but unhandled exception reached the topmost exception handler!{{NOCOLOR}} {{indent}}{{YELLOW}}Exception type:{{NOCOLOR}} {{DARK_RED}}{ex.__class__.__name__}{{NOCOLOR}} {{indent}}{{YELLOW}}Exception message:{{NOCOLOR}} {{RED}}{ex!s}{{NOCOLOR}} {{indent}}{{YELLOW}}Caused in:{{NOCOLOR}} {funcName}(...) in file '{filename}' at line {sourceLine}\ """).format(indent=self.INDENT, **self.Foreground)) if ex.__cause__ is not None: self.WriteLineToStdErr(dedent(f"""\ {{indent2}}{{DARK_YELLOW}}Caused by ex. type:{{NOCOLOR}} {{DARK_RED}}{ex.__cause__.__class__.__name__}{{NOCOLOR}} {{indent2}}{{DARK_YELLOW}}Caused by message:{{NOCOLOR}} {{RED}}{ex.__cause__!s}{{NOCOLOR}}\ """).format(indent2=self.INDENT * 2, **self.Foreground)) self.WriteLineToStdErr(f"""{{indent}}{{RED}}{'-' * 80}{{NOCOLOR}}""".format(indent=self.INDENT, **self.Foreground)) print_tb(ex.__traceback__, file=self._stderr) self.WriteLineToStdErr(f"""{{indent}}{{RED}}{'-' * 80}{{NOCOLOR}}""".format(indent=self.INDENT, **self.Foreground)) if self.ISSUE_TRACKER_URL is not None: self.WriteLineToStdErr(dedent(f"""\ {{indent}}{{DARK_CYAN}}Please report this bug at GitHub: {self.ISSUE_TRACKER_URL}{{NOCOLOR}} {{indent}}{{RED}}{'-' * 80}{{NOCOLOR}}\ """).format(indent=self.INDENT, **self.Foreground)) self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE) @export @unique class Severity(Enum): """Logging message severity levels.""" Fatal = 100 #: Fatal messages Error = 80 #: Error messages Quiet = 70 #: Always visible messages, even in quiet mode. Critical = 60 #: Critical messages Warning = 50 #: Warning messages Info = 20 #: Informative messages Normal = 10 #: Normal messages DryRun = 8 #: Messages visible in a dry-run Verbose = 5 #: Verbose messages Debug = 2 #: Debug messages All = 0 #: All messages def __hash__(self) -> int: return hash(self.name) def __eq__(self, other: Any) -> bool: """ Compare two Severity instances (severity level) for equality. :param other: Operand to compare against. :returns: ``True``, if both severity levels are equal. :raises TypeError: If operand ``other`` is not of type :class:`Severity`. """ if isinstance(other, Severity): return self.value == other.value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.") ex.add_note(f"Supported types for second operand: Severity") raise ex def __ne__(self, other: Any) -> bool: """ Compare two Severity instances (severity level) for inequality. :param other: Operand to compare against. :returns: ``True``, if both severity levels are unequal. :raises TypeError: If operand ``other`` is not of type :class:`Severity`. """ if isinstance(other, Severity): return self.value != other.value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by != operator.") ex.add_note(f"Supported types for second operand: Severity") raise ex def __lt__(self, other: Any) -> bool: """ Compare two Severity instances (severity level) for less-than. :param other: Operand to compare against. :returns: ``True``, if severity levels is less than other severity level. :raises TypeError: If operand ``other`` is not of type :class:`Severity`. """ if isinstance(other, Severity): return self.value < other.value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.") ex.add_note(f"Supported types for second operand: Severity") raise ex def __le__(self, other: Any) -> bool: """ Compare two Severity instances (severity level) for less-than-or-equal. :param other: Operand to compare against. :returns: ``True``, if severity levels is less than or equal other severity level. :raises TypeError: If operand ``other`` is not of type :class:`Severity`. """ if isinstance(other, Severity): return self.value <= other.value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.") ex.add_note(f"Supported types for second operand: Severity") raise ex def __gt__(self, other: Any) -> bool: """ Compare two Severity instances (severity level) for greater-than. :param other: Operand to compare against. :returns: ``True``, if severity levels is greater than other severity level. :raises TypeError: If operand ``other`` is not of type :class:`Severity`. """ if isinstance(other, Severity): return self.value > other.value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.") ex.add_note(f"Supported types for second operand: Severity") raise ex def __ge__(self, other: Any) -> bool: """ Compare two Severity instances (severity level) for greater-than-or-equal. :param other: Operand to compare against. :returns: ``True``, if severity levels is greater than or equal other severity level. :raises TypeError: If operand ``other`` is not of type :class:`Severity`. """ if isinstance(other, Severity): return self.value >= other.value else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.") ex.add_note(f"Supported types for second operand: Severity") raise ex @export @unique class Mode(Enum): TextToStdOut_ErrorsToStdErr = 0 AllLinearToStdOut = 1 DataToStdOut_OtherToStdErr = 2 @export class Line(metaclass=ExtendedType, slots=True): """Represents a single message line with a severity and indentation level.""" _LOG_MESSAGE_FORMAT__ = { Severity.Fatal: "FATAL: {message}", Severity.Error: "ERROR: {message}", Severity.Quiet: "{message}", Severity.Warning: "WARNING: {message}", Severity.Info: "INFO: {message}", Severity.Normal: "{message}", Severity.DryRun: "DRYRUN: {message}", Severity.Verbose: "VERBOSE: {message}", Severity.Debug: "DEBUG: {message}", } #: Message line formatting rules. _message: str #: Text message (line content). _severity: Severity #: Message severity _indent: int #: Indentation _appendLinebreak: bool #: True, if a trailing linebreak should be added when printing this line object. def __init__(self, message: str, severity: Severity = Severity.Normal, indent: int = 0, appendLinebreak: bool = True) -> None: """Constructor for a new ``Line`` object.""" self._severity = severity self._message = message self._indent = indent self._appendLinebreak = appendLinebreak @readonly def Message(self) -> str: """ Return the indented line. :returns: Raw message of the line. """ return self._message @readonly def Severity(self) -> Severity: """ Return the line's severity level. :returns: Severity level of the message line. """ return self._severity @readonly def Indent(self) -> int: """ Return the line's indentation level. :returns: Indentation level. """ return self._indent def IndentBy(self, indent: int) -> int: """ Increase a line's indentation level. :param indent: Indentation level added to the current indentation level. """ # TODO: used named expression starting from Python 3.8 indent += self._indent self._indent = indent return indent @readonly def AppendLinebreak(self) -> bool: """ Returns if a linebreak should be added at the end of the message. :returns: True, if a linebreak should be added. """ return self._appendLinebreak def __str__(self) -> str: """Returns a formatted version of a ``Line`` objects as a string.""" return self._LOG_MESSAGE_FORMAT__[self._severity].format(message=self._message) @export @mixin class ILineTerminal: """A mixin class (interface) to provide class-local terminal writing methods.""" _terminal: TerminalBaseApplication def __init__(self, terminal: Nullable[TerminalBaseApplication] = None) -> None: """MixIn initializer.""" self._terminal = terminal # FIXME: Alter methods if a terminal is present or set dummy methods @readonly def Terminal(self) -> TerminalBaseApplication: """Return the local terminal instance.""" return self._terminal def WriteLine(self, line: Line, condition: bool = True) -> bool: """Write an entry to the local terminal.""" if (self._terminal is not None) and condition: return self._terminal.WriteLine(line) return False # def _TryWriteLine(self, *args: Any, condition: bool = True, **kwargs: Any): # if (self._terminal is not None) and condition: # return self._terminal.TryWrite(*args, **kwargs) # return False def WriteFatal(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write a fatal message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteFatal(*args, **kwargs) return False def WriteError(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write an error message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteError(*args, **kwargs) return False def WriteCritical(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write a warning message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteCritical(*args, **kwargs) return False def WriteWarning(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write a warning message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteWarning(*args, **kwargs) return False def WriteInfo(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write an info message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteInfo(*args, **kwargs) return False def WriteQuiet(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write a message even in quiet mode if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteQuiet(*args, **kwargs) return False def WriteNormal(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write a *normal* message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteNormal(*args, **kwargs) return False def WriteVerbose(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write a verbose message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteVerbose(*args, **kwargs) return False def WriteDebug(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write a debug message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteDebug(*args, **kwargs) return False def WriteDryRun(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: """Write a dry-run message if ``condition`` is true.""" if (self._terminal is not None) and condition: return self._terminal.WriteDryRun(*args, **kwargs) return False @export class TerminalApplication(TerminalBaseApplication): #, ILineTerminal): """ A base-class for implementation of terminal applications emitting line-by-line messages. """ _LOG_MESSAGE_FORMAT__ = { Severity.Fatal: "{DARK_RED}[FATAL] {message}{NOCOLOR}", Severity.Error: "{RED}[ERROR] {message}{NOCOLOR}", Severity.Quiet: "{WHITE}{message}{NOCOLOR}", Severity.Critical: "{DARK_YELLOW}[CRITICAL] {message}{NOCOLOR}", Severity.Warning: "{YELLOW}[WARNING] {message}{NOCOLOR}", Severity.Info: "{WHITE}{message}{NOCOLOR}", Severity.Normal: "{WHITE}{message}{NOCOLOR}", Severity.DryRun: "{DARK_CYAN}[DRY] {message}{NOCOLOR}", Severity.Verbose: "{GRAY}{message}{NOCOLOR}", Severity.Debug: "{DARK_GRAY}{message}{NOCOLOR}" } #: Message formatting rules. _LOG_LEVEL_ROUTING__: Dict[Severity, Tuple[Callable[[str, str], int]]] #: Message routing rules. _verbose: bool _debug: bool _quiet: bool _writeLevel: Severity _writeToStdOut: bool _lines: List[Line] _baseIndent: int _errorCount: int _criticalWarningCount: int _warningCount: int HeadLine: ClassVar[str] def __init__(self, mode: Mode = Mode.AllLinearToStdOut) -> None: """ Initializer of a line-based terminal interface. :param mode: Defines what output (normal, error, data) to write where. Default: a linear flow all to *STDOUT*. """ TerminalBaseApplication.__init__(self) # ILineTerminal.__init__(self, self) self._LOG_LEVEL_ROUTING__ = {} self.__InitializeLogLevelRouting(mode) self._verbose = False self._debug = False self._quiet = False self._writeLevel = Severity.Normal self._writeToStdOut = True self._lines = [] self._baseIndent = 0 self._errorCount = 0 self._criticalWarningCount = 0 self._warningCount = 0 def __InitializeLogLevelRouting(self, mode: Mode) -> None: if mode is Mode.TextToStdOut_ErrorsToStdErr: for severity in Severity: if severity >= Severity.Warning and severity != Severity.Quiet: self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdErr,) else: self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdOut,) elif mode is Mode.AllLinearToStdOut: for severity in Severity: self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdOut, ) elif mode is Mode.DataToStdOut_OtherToStdErr: for severity in Severity: self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdErr, ) else: # pragma: no cover raise ExceptionBase(f"Unsupported mode '{mode}'.") def _PrintHeadline(self, width: int = 80) -> None: """ Helper method to print the program headline. :param width: Number of characters for horizontal lines. .. admonition:: Generated output .. code-block:: ========================= centered headline ========================= """ if width == 0: width = self._width self.WriteNormal(f"{{HEADLINE}}{'=' * width}".format(**TerminalApplication.Foreground)) self.WriteNormal(f"{{HEADLINE}}{{headline: ^{width}s}}".format(headline=self.HeadLine, **TerminalApplication.Foreground)) self.WriteNormal(f"{{HEADLINE}}{'=' * width}".format(**TerminalApplication.Foreground)) def _PrintVersion(self, author: str, email: str, copyright: str, license: str, version: str) -> None: """ Helper method to print the version information. :param author: Author of the application. :param email: The author's email address. :param copyright: The copyright information. :param license: The license. :param version: The application's version. .. admonition:: Example usage .. code-block:: Python def _PrintVersion(self): from MyModule import __author__, __email__, __copyright__, __license__, __version__ super()._PrintVersion(__author__, __email__, __copyright__, __license__, __version__) """ self.WriteNormal(f"Author: {author} ({email})") self.WriteNormal(f"Copyright: {copyright}") self.WriteNormal(f"License: {license}") self.WriteNormal(f"Version: {version}") def Configure(self, verbose: bool = False, debug: bool = False, quiet: bool = False, writeToStdOut: bool = True) -> None: self._verbose = True if debug else verbose self._debug = debug self._quiet = quiet if quiet: self._writeLevel = Severity.Quiet elif debug: self._writeLevel = Severity.Debug elif verbose: self._writeLevel = Severity.Verbose else: self._writeLevel = Severity.Normal self._writeToStdOut = writeToStdOut @readonly def Verbose(self) -> bool: """Returns true, if verbose messages are enabled.""" return self._verbose @readonly def Debug(self) -> bool: """Returns true, if debug messages are enabled.""" return self._debug @readonly def Quiet(self) -> bool: """Returns true, if quiet mode is enabled.""" return self._quiet @property def LogLevel(self) -> Severity: """Return the current minimal severity level for writing.""" return self._writeLevel @LogLevel.setter def LogLevel(self, value: Severity) -> None: """Set the minimal severity level for writing.""" self._writeLevel = value @property def BaseIndent(self) -> int: return self._baseIndent @BaseIndent.setter def BaseIndent(self, value: int) -> None: self._baseIndent = value @readonly def WarningCount(self) -> int: return self._warningCount @readonly def CriticalWarningCount(self) -> int: return self._criticalWarningCount @readonly def ErrorCount(self) -> int: return self._errorCount @readonly def Lines(self) -> List[Line]: return self._lines def ExitOnPreviousErrors(self) -> None: """Exit application if errors have been printed.""" if self._errorCount > 0: self.WriteFatal("Too many errors in previous steps.") def ExitOnPreviousCriticalWarnings(self, includeErrors: bool = True) -> None: """Exit application if critical warnings have been printed.""" if includeErrors and (self._errorCount > 0): if self._criticalWarningCount > 0: self.WriteFatal("Too many errors and critical warnings in previous steps.") else: self.WriteFatal("Too many errors in previous steps.") elif self._criticalWarningCount > 0: self.WriteFatal("Too many critical warnings in previous steps.") def ExitOnPreviousWarnings(self, includeCriticalWarnings: bool = True, includeErrors: bool = True) -> None: """Exit application if warnings have been printed.""" if includeErrors and (self._errorCount > 0): if includeCriticalWarnings and (self._criticalWarningCount > 0): if self._warningCount > 0: self.WriteFatal("Too many errors and (critical) warnings in previous steps.") else: self.WriteFatal("Too many errors and critical warnings in previous steps.") elif self._warningCount > 0: self.WriteFatal("Too many warnings in previous steps.") else: self.WriteFatal("Too many errors in previous steps.") elif includeCriticalWarnings and (self._criticalWarningCount > 0): if self._warningCount > 0: self.WriteFatal("Too many (critical) warnings in previous steps.") else: self.WriteFatal("Too many critical warnings in previous steps.") elif self._warningCount > 0: self.WriteFatal("Too many warnings in previous steps.") def WriteLine(self, line: Line) -> bool: """Print a formatted line to the underlying terminal/console offered by the operating system.""" if line.Severity >= self._writeLevel: self._lines.append(line) for method in self._LOG_LEVEL_ROUTING__[line.Severity]: method(self._LOG_MESSAGE_FORMAT__[line.Severity].format(message=line.Message, **self.Foreground), end="\n" if line.AppendLinebreak else "") return True else: return False def TryWriteLine(self, line) -> bool: return line.Severity >= self._writeLevel def WriteFatal(self, message: str, indent: int = 0, appendLinebreak: bool = True, immediateExit: bool = True) -> bool: ret = self.WriteLine(Line(message, Severity.Fatal, self._baseIndent + indent, appendLinebreak)) if immediateExit: self.FatalExit() return ret def WriteError(self, message: str, indent: int = 0, appendLinebreak: bool = True) -> bool: self._errorCount += 1 return self.WriteLine(Line(message, Severity.Error, self._baseIndent + indent, appendLinebreak)) def WriteCritical(self, message: str, indent: int = 0, appendLinebreak: bool = True) -> bool: self._criticalWarningCount += 1 return self.WriteLine(Line(message, Severity.Critical, self._baseIndent + indent, appendLinebreak)) def WriteWarning(self, message: str, indent: int = 0, appendLinebreak: bool = True) -> bool: self._warningCount += 1 return self.WriteLine(Line(message, Severity.Warning, self._baseIndent + indent, appendLinebreak)) def WriteInfo(self, message: str, indent: int = 0, appendLinebreak: bool = True) -> bool: return self.WriteLine(Line(message, Severity.Info, self._baseIndent + indent, appendLinebreak)) def WriteQuiet(self, message: str, indent: int = 0, appendLinebreak: bool = True) -> bool: return self.WriteLine(Line(message, Severity.Quiet, self._baseIndent + indent, appendLinebreak)) def WriteNormal(self, message: str, indent: int = 0, appendLinebreak: bool = True) -> bool: """ Write a normal message. Depending on internal settings and rules, a message might be skipped. :param message: Message to write. :param indent: Indentation level of the message. :param appendLinebreak: Append a linebreak after the message. Default: ``True`` :return: True, if message was actually written. """ return self.WriteLine(Line(message, Severity.Normal, self._baseIndent + indent, appendLinebreak)) def WriteVerbose(self, message: str, indent: int = 1, appendLinebreak: bool = True) -> bool: return self.WriteLine(Line(message, Severity.Verbose, self._baseIndent + indent, appendLinebreak)) def WriteDebug(self, message: str, indent: int = 2, appendLinebreak: bool = True) -> bool: return self.WriteLine(Line(message, Severity.Debug, self._baseIndent + indent, appendLinebreak)) def WriteDryRun(self, message: str, indent: int = 2, appendLinebreak: bool = True) -> bool: return self.WriteLine(Line(message, Severity.DryRun, self._baseIndent + indent, appendLinebreak)) pyTooling-8.11.0/pyTooling/Tracing/000077500000000000000000000000001513317154500171455ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Tracing/__init__.py000066400000000000000000000454061513317154500212670ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ __ _ ___(_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _` |/ __| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | (_| | (__| | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \__,_|\___|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Tools for software execution tracing.""" from datetime import datetime from time import perf_counter_ns from threading import local from types import TracebackType from typing import Optional as Nullable, List, Iterator, Type, Self, Iterable, Dict, Any, Tuple try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyTooling.Exceptions import ToolingException from pyTooling.Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Tracing] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType from Exceptions import ToolingException from Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Tracing] Could not import directly!") raise ex __all__ = ["_threadLocalData"] _threadLocalData = local() """A reference to the thread local data needed by the pyTooling.Tracing classes.""" @export class TracingException(ToolingException): """Base-exception of all exceptions raised by :mod:`pyTooling.Tracing`.""" @export class Event(metaclass=ExtendedType, slots=True): """ Represents a named event within a timespan (:class:`Span`) used in a software execution trace. It may contain arbitrary attributes (key-value pairs). """ _name: str #: Name of the event. _parent: Nullable["Span"] #: Reference to the parent span. _time: Nullable[datetime] #: Timestamp of the event. _dict: Dict[str, Any] #: Dictionary of associated attributes. def __init__(self, name: str, time: Nullable[datetime] = None, parent: Nullable["Span"] = None) -> None: """ Initializes a named event. :param name: The name of the event. :param time: The optional time when the event happened. :param parent: Reference to the parent span. """ if isinstance(name, str): if name == "": raise ValueError(f"Parameter 'name' is empty.") self._name = name else: ex = TypeError("Parameter 'name' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") raise ex if time is None: self._time = None elif isinstance(time, datetime): self._time = time else: ex = TypeError("Parameter 'time' is not of type 'datetime'.") ex.add_note(f"Got type '{getFullyQualifiedName(time)}'.") raise ex if parent is None: self._parent = None elif isinstance(parent, Span): self._parent = parent parent._events.append(self) else: ex = TypeError("Parameter 'parent' is not of type 'Span'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex self._dict = {} @readonly def Name(self) -> str: """ Read-only property to access the event's name. :returns: Name of the event. """ return self._name @readonly def Time(self) -> datetime: """ Read-only property to access the event's timestamp. :returns: Timestamp of the event. """ return self._time @readonly def Parent(self) -> Nullable["Span"]: """ Read-only property to access the event's parent span. :returns: Parent span. """ return self._parent def __getitem__(self, key: str) -> Any: """ Read an event's attached attributes (key-value-pairs) by key. :param key: The key to look for. :returns: The value associated to the given key. """ return self._dict[key] def __setitem__(self, key: str, value: Any) -> None: """ Create or update an event's attached attributes (key-value-pairs) by key. If a key doesn't exist yet, a new key-value-pair is created. :param key: The key to create or update. :param value: The value to associate to the given key. """ self._dict[key] = value def __delitem__(self, key: str) -> None: """ Remove an entry from event's attached attributes (key-value-pairs) by key. :param key: The key to remove. :raises KeyError: If key doesn't exist in the event's attributes. """ del self._dict[key] def __contains__(self, key: str) -> bool: """ Checks if the key is an attached attribute (key-value-pairs) on this event. :param key: The key to check. :returns: ``True``, if the key is an attached attribute. """ return key in self._dict def __iter__(self) -> Iterator[Tuple[str, Any]]: """ Returns an iterator to iterate all associated attributes of this event as :pycode:`(key, value)` tuples. :returns: Iterator to iterate all attributes. """ return iter(self._dict.items()) def __len__(self) -> int: """ Returns the number of attached attributes (key-value-pairs) on this event. :returns: Number of attached attributes. """ return len(self._dict) def __str__(self) -> str: """ Return a string representation of the event. :returns: The event's name. """ return self._name @export class Span(metaclass=ExtendedType, slots=True): """ Represents a timespan (span) within another timespan or trace. It may contain sub-spans, events and arbitrary attributes (key-value pairs). """ _name: str #: Name of the timespan _parent: Nullable["Span"] #: Reference to the parent span (or trace). _beginTime: Nullable[datetime] #: Timestamp when the timespan begins. _endTime: Nullable[datetime] #: Timestamp when the timespan ends. _startTime: Nullable[int] _stopTime: Nullable[int] _totalTime: Nullable[int] #: Duration of this timespan in ns. _spans: List["Span"] #: Sub-timespans _events: List[Event] #: Events happened within this timespan _dict: Dict[str, Any] #: Dictionary of associated attributes. def __init__(self, name: str, parent: Nullable["Span"] = None) -> None: """ Initializes a timespan as part of a software execution trace. :param name: Name of the timespan. :param parent: Reference to a parent span or trace. """ if isinstance(name, str): if name == "": raise ValueError(f"Parameter 'name' is empty.") self._name = name else: ex = TypeError("Parameter 'name' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex if parent is None: self._parent = None elif isinstance(parent, Span): self._parent = parent parent._spans.append(self) else: ex = TypeError("Parameter 'parent' is not of type 'Span'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex self._beginTime = None self._startTime = None self._endTime = None self._stopTime = None self._totalTime = None self._spans = [] self._events = [] self._dict = {} @readonly def Name(self) -> str: """ Read-only property to access the timespan's name. :returns: Name of the timespan. """ return self._name @readonly def Parent(self) -> Nullable["Span"]: """ Read-only property to access the span's parent span or trace. :returns: Parent span. """ return self._parent def _AddSpan(self, span: "Span") -> Self: self._spans.append(span) span._parent = self return span @readonly def HasSubSpans(self) -> bool: """ Check if this timespan contains nested sub-spans. :returns: ``True``, if the span has nested spans. """ return len(self._spans) > 0 @readonly def SubSpanCount(self) -> int: """ Return the number of sub-spans within this span. :return: Number of nested spans. """ return len(self._spans) # iterate subspans with optional predicate def IterateSubSpans(self) -> Iterator["Span"]: """ Returns an iterator to iterate all nested sub-spans. :returns: Iterator to iterate all sub-spans. """ return iter(self._spans) @readonly def HasEvents(self) -> bool: """ Check if this timespan contains events. :returns: ``True``, if the span has events. """ return len(self._events) > 0 @readonly def EventCount(self) -> int: """ Return the number of events within this span. :return: Number of events. """ return len(self._events) # iterate events with optional predicate def IterateEvents(self) -> Iterator[Event]: """ Returns an iterator to iterate all embedded events. :returns: Iterator to iterate all events. """ return iter(self._events) @readonly def StartTime(self) -> Nullable[datetime]: """ Read-only property accessing the absolute time when the span was started. :return: The time when the span was entered, otherwise None. """ return self._beginTime @readonly def StopTime(self) -> Nullable[datetime]: """ Read-only property accessing the absolute time when the span was stopped. :return: The time when the span was exited, otherwise None. """ return self._endTime @readonly def Duration(self) -> float: """ Read-only property accessing the duration from start operation to stop operation. If the span is not yet stopped, the duration from start to now is returned. :return: Duration since span was started in seconds. :raises TracingException: When span was never started. """ if self._startTime is None: raise TracingException(f"{self.__class__.__name__} was never started.") return ((perf_counter_ns() - self._startTime) if self._stopTime is None else self._totalTime) / 1e9 @classmethod def CurrentSpan(cls) -> "Span": """ Class-method to return the currently active timespan (span) or ``None``. :returns: Currently active span or ``None``. """ global _threadLocalData try: currentSpan = _threadLocalData.currentSpan except AttributeError: currentSpan = None return currentSpan def __enter__(self) -> Self: """ Implementation of the :ref:`context manager protocol's ` ``__enter__(...)`` method. A span will be started. :return: The span itself. """ global _threadLocalData try: currentSpan = _threadLocalData.currentSpan except AttributeError: ex = TracingException("Can't setup span. No active trace.") ex.add_note("Use with-statement using 'Trace()' to setup software execution tracing.") raise ex _threadLocalData.currentSpan = currentSpan._AddSpan(self) self._beginTime = datetime.now() self._startTime = perf_counter_ns() return self def __exit__( self, exc_type: Nullable[Type[BaseException]] = None, exc_val: Nullable[BaseException] = None, exc_tb: Nullable[TracebackType] = None ) -> Nullable[bool]: """ Implementation of the :ref:`context manager protocol's ` ``__exit__(...)`` method. An active span will be stopped. Exit the context and ...... :param exc_type: Exception type :param exc_val: Exception instance :param exc_tb: Exception's traceback. :returns: ``None`` """ global _threadLocalData self._stopTime = perf_counter_ns() self._endTime = datetime.now() self._totalTime = self._stopTime - self._startTime currentSpan = _threadLocalData.currentSpan _threadLocalData.currentSpan = currentSpan._parent def __getitem__(self, key: str) -> Any: """ Read an event's attached attributes (key-value-pairs) by key. :param key: The key to look for. :returns: The value associated to the given key. """ return self._dict[key] def __setitem__(self, key: str, value: Any) -> None: """ Create or update an event's attached attributes (key-value-pairs) by key. If a key doesn't exist yet, a new key-value-pair is created. :param key: The key to create or update. :param value: The value to associate to the given key. """ self._dict[key] = value def __delitem__(self, key: str) -> None: """ Remove an entry from event's attached attributes (key-value-pairs) by key. :param key: The key to remove. :raises KeyError: If key doesn't exist in the event's attributes. """ del self._dict[key] def __contains__(self, key: str) -> bool: """ Checks if the key is an attached attribute (key-value-pairs) on this event. :param key: The key to check. :returns: ``True``, if the key is an attached attribute. """ return key in self._dict def __iter__(self) -> Iterator[Tuple[str, Any]]: """ Returns an iterator to iterate all associated attributes of this timespan as :pycode:`(key, value)` tuples. :returns: Iterator to iterate all attributes. """ return iter(self._dict.items()) def __len__(self) -> int: """ Returns the number of attached attributes (key-value-pairs) on this event. :returns: Number of attached attributes. """ return len(self._dict) def Format(self, indent: int = 1, columnSize: int = 25) -> Iterable[str]: result = [] result.append(f"{' ' * indent}🕑{self._name:<{columnSize - 2 * indent}} {self._totalTime/1e6:8.3f} ms") for span in self._spans: result.extend(span.Format(indent + 1, columnSize)) return result def __repr__(self) -> str: return f"{self._name} -> {self._parent!r}" def __str__(self) -> str: """ Return a string representation of the timespan. :returns: The span's name. """ return self._name @export class Trace(Span): """ Represents a software execution trace made up of timespans (:class:`Span`). The trace is the top-most element in a tree of timespans. All timespans share the same *TraceID*, thus even in a distributed software execution, timespans can be aggregated with delay in a centralized database and the flow of execution can be reassembled by grouping all timespans with same *TraceID*. Execution order can be derived from timestamps and parallel execution is represented by overlapping timespans sharing the same parent *SpanID*. Thus, the tree structure can be reassembled by inspecting the parent *SpanID* relations within the same *TraceID*. A trace may contain sub-spans, events and arbitrary attributes (key-value pairs). """ def __init__(self, name: str) -> None: """ Initializes a software execution trace. :param name: Name of the trace. """ super().__init__(name) def __enter__(self) -> Self: global _threadLocalData # TODO: check if a trace is already setup # try: # currentTrace = _threadLocalData.currentTrace # except AttributeError: # pass _threadLocalData.currentTrace = self _threadLocalData.currentSpan = self self._beginTime = datetime.now() self._startTime = perf_counter_ns() return self def __exit__( self, exc_type: Nullable[Type[BaseException]] = None, exc_val: Nullable[BaseException] = None, exc_tb: Nullable[TracebackType] = None ) -> Nullable[bool]: """ Exit the context and ...... :param exc_type: Exception type :param exc_val: Exception instance :param exc_tb: Exception's traceback. :returns: ``None`` """ global _threadLocalData self._stopTime = perf_counter_ns() self._endTime = datetime.now() self._totalTime = self._stopTime - self._startTime del _threadLocalData.currentTrace del _threadLocalData.currentSpan return None @classmethod def CurrentTrace(cls) -> "Trace": """ Class-method to return the currently active trace or ``None``. :returns: Currently active trace or ``None``. """ try: currentTrace = _threadLocalData.currentTrace except AttributeError: currentTrace = None return currentTrace def Format(self, indent: int = 0, columnSize: int = 25) -> Iterable[str]: result = [] result.append(f"{' ' * indent}Software Execution Trace: {self._totalTime/1e6:8.3f} ms") result.append(f"{' ' * indent}📉{self._name:<{columnSize - 2}} {self._totalTime/1e6:8.3f} ms") for span in self._spans: result.extend(span.Format(indent + 1, columnSize - 2)) return result pyTooling-8.11.0/pyTooling/Tree/000077500000000000000000000000001513317154500164555ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Tree/__init__.py000066400000000000000000001066731513317154500206030ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _ \/ _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | __/ __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \___|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """A powerful tree data structure for Python.""" from collections import deque from typing import TypeVar, Generic, List, Tuple, Dict, Deque, Union, Optional as Nullable from typing import Callable, Iterator, Generator, Iterable, Mapping, Hashable try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyTooling.Exceptions import ToolingException from pyTooling.Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Tree] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, mixin from Exceptions import ToolingException from Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Tree] Could not import directly!") raise ex IDType = TypeVar("IDType", bound=Hashable) """A type variable for a tree's ID.""" ValueType = TypeVar("ValueType") """A type variable for a tree's value.""" DictKeyType = TypeVar("DictKeyType") """A type variable for a tree's dictionary keys.""" DictValueType = TypeVar("DictValueType") """A type variable for a tree's dictionary values.""" @export class TreeException(ToolingException): """Base exception of all exceptions raised by :mod:`pyTooling.Tree`.""" @export class InternalError(TreeException): """ The exception is raised when a data structure corruption is detected. .. danger:: This exception should never be raised. If so, please create an issue at GitHub so the data structure corruption can be investigated and fixed. |br| `⇒ Bug Tracker at GitHub `__ """ @export class NoSiblingsError(TreeException): """ The exception is raised when a node has no parent and thus has no siblings. .. hint:: A node with no parent is the root node of the tree. """ @export class AlreadyInTreeError(TreeException): """ The exception is raised when the current node and the other node are already in the same tree. .. hint:: A tree a an acyclic graph without cross-edges. Thus backward edges and cross edges are permitted. """ @export class NotInSameTreeError(TreeException): """The exception is raised when the current node and the other node are not in the same tree.""" @export class Node(Generic[IDType, ValueType, DictKeyType, DictValueType], metaclass=ExtendedType, slots=True): """ A **tree** data structure can be constructed of ``Node`` instances. Therefore, nodes can be connected to parent nodes or a parent node can add child nodes. This allows to construct a tree top-down or bottom-up. .. hint:: The top-down construction should be preferred, because it's slightly faster. Each tree uses the **root** node (a.k.a. tree-representative) to store some per-tree data structures. E.g. a list of all IDs in a tree. For easy and quick access to such data structures, each sibling node contains a reference to the root node (:attr:`_root`). In case of adding a tree to an existing tree, such data structures get merged and all added nodes get assigned with new root references. Use the read-only property :attr:`Root` to access the root reference. The reference to the parent node (:attr:`_parent`) can be access via property :attr:`Parent`. If the property's setter is used, a node and all its siblings are added to another tree or to a new position in the same tree. The references to all node's children is stored in a list (:attr:`_children`). Children, siblings, ancestors, can be accessed via various generators: * :meth:`GetAncestors` |rarr| iterate all ancestors bottom-up. * :meth:`GetChildren` |rarr| iterate all direct children. * :meth:`GetDescendants` |rarr| iterate all descendants. * :meth:`IterateLevelOrder` |rarr| IterateLevelOrder. * :meth:`IteratePreOrder` |rarr| iterate siblings in pre-order. * :meth:`IteratePostOrder` |rarr| iterate siblings in post-order. Each node can have a **unique ID** or no ID at all (``nodeID=None``). The root node is used to store all IDs in a dictionary (:attr:`_nodesWithID`). In case no ID is given, all such ID-less nodes are collected in a single bin and store as a list of nodes. An ID can be modified after the Node was created. Use the read-only property :attr:`ID` to access the ID. Each node can have a **value** (:attr:`_value`), which can be given at node creation time, or it can be assigned and/or modified later. Use the property :attr:`Value` to get or set the value. Moreover, each node can store various key-value-pairs (:attr:`_dict`). Use the dictionary syntax to get and set key-value-pairs. """ _id: Nullable[IDType] #: Unique identifier of a node. ``None`` if not used. _nodesWithID: Nullable[Dict[IDType, 'Node']] #: Dictionary of all IDs in the tree. ``None`` if it's not the root node. _nodesWithoutID: Nullable[List['Node']] #: List of all nodes without an ID in the tree. ``None`` if it's not the root node. _root: 'Node' #: Reference to the root of a tree. ``self`` if it's the root node. _parent: Nullable['Node'] #: Reference to the parent node. ``None`` if it's the root node. _children: List['Node'] #: List of all children # _links: List['Node'] _level: int #: Level of the node (distance to the root). _value: Nullable[ValueType] #: Field to store the node's value. _dict: Dict[DictKeyType, DictValueType] #: Dictionary to store key-value-pairs attached to the node. _format: Nullable[Callable[["Node"], str]] #: A node formatting function returning a one-line representation for tree-rendering. def __init__( self, nodeID: Nullable[IDType] = None, value: Nullable[ValueType] = None, keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, parent: 'Node' = None, children: Nullable[Iterable['Node']] = None, format: Nullable[Callable[["Node"], str]] = None ) -> None: """ .. todo:: TREE::Node::init Needs documentation. :param nodeID: The optional unique ID of a node within the whole tree data structure. :param value: The optional value of the node. :param keyValuePairs: The optional mapping (dictionary) of key-value-pairs. :param parent: The optional parent node in the tree. :param children: The optional list of child nodes. :param format: The optional node formatting function returning a one-line representation for tree-rendering. :raises TypeError: If parameter parent is not an instance of Node. :raises ValueError: If nodeID already exists in the tree. :raises TypeError: If parameter children is not iterable. :raises ValueError: If an element of children is not an instance of Node. """ self._id = nodeID self._value = value self._dict = {key: value for key, value in keyValuePairs.items()} if keyValuePairs is not None else {} self._format = format if parent is not None and not isinstance(parent, Node): ex = TypeError("Parameter 'parent' is not of type 'Node'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex if parent is None: self._root = self self._parent = None self._level = 0 self._nodesWithID = {} self._nodesWithoutID = [] if nodeID is None: self._nodesWithoutID.append(self) else: self._nodesWithID[nodeID] = self else: self._root = parent._root self._parent = parent self._level = parent._level + 1 self._nodesWithID = None self._nodesWithoutID = None if nodeID is None: self._root._nodesWithoutID.append(self) elif nodeID in self._root._nodesWithID: raise ValueError(f"ID '{nodeID}' already exists in this tree.") else: self._root._nodesWithID[nodeID] = self parent._children.append(self) self._children = [] if children is not None: if not isinstance(children, Iterable): ex = TypeError("Parameter 'children' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(children)}'.") raise ex for child in children: if not isinstance(child, Node): ex = TypeError(f"Item '{child}' in parameter 'children' is not of type 'Node'.") ex.add_note(f"Got type '{getFullyQualifiedName(child)}'.") raise ex child.Parent = self @readonly def ID(self) -> Nullable[IDType]: """ Read-only property to access the unique ID of a node (:attr:`_id`). If no ID was given at node construction time, ID return None. :returns: Unique ID of a node, if ID was given at node creation time, else None. """ return self._id @property def Value(self) -> Nullable[ValueType]: """ Property to get and set the value (:attr:`_value`) of a node. :returns: The value of a node. """ return self._value @Value.setter def Value(self, value: Nullable[ValueType]) -> None: self._value = value def __getitem__(self, key: DictKeyType) -> DictValueType: """ Read a node's attached attributes (key-value-pairs) by key. :param key: The key to look for. :returns: The value associated to the given key. """ return self._dict[key] def __setitem__(self, key: DictKeyType, value: DictValueType) -> None: """ Create or update a node's attached attributes (key-value-pairs) by key. If a key doesn't exist yet, a new key-value-pair is created. :param key: The key to create or update. :param value: The value to associate to the given key. """ self._dict[key] = value def __delitem__(self, key: DictKeyType) -> None: """ .. todo:: TREE::Node::__delitem__ Needs documentation. """ del self._dict[key] def __contains__(self, key: DictKeyType) -> bool: """ .. todo:: TREE::Node::__contains__ Needs documentation. """ return key in self._dict def __len__(self) -> int: """ Returns the number of attached attributes (key-value-pairs) on this node. :returns: Number of attached attributes. """ return len(self._dict) @readonly def Root(self) -> 'Node': """ Read-only property to access the tree's root node (:attr:`_root`). :returns: The root node (representative node) of a tree. """ return self._root @property def Parent(self) -> Nullable['Node']: """ Property to get and set the parent (:attr:`_parent`) of a node. .. note:: As the current node might be a tree itself, appending this node to a tree can lead to a merge of trees and especially to a merge of IDs. As IDs are unique, it might raise an :exc:`Exception`. :returns: The parent of a node. :raises TypeError: If parameter ``parent`` is not a :class:`Node` :raises AlreadyInTreeError: Parent is already a child node in this tree. """ return self._parent @Parent.setter def Parent(self, parent: Nullable['Node']) -> None: # TODO: is moved inside the same tree, don't move nodes in _nodesWithID and don't change _root if parent is None: self._nodesWithID = {} self._nodesWithoutID = [] self._level = 0 if self._id is None: self._nodesWithoutID.append(self) self._root._nodesWithoutID.remove(self) else: self._nodesWithID[self._id] = self del self._nodesWithID[self._id] for sibling in self.GetDescendants(): sibling._root = self sibling._level = sibling._parent._level + 1 if sibling._id is None: self._nodesWithoutID.append(sibling) self._root._nodesWithoutID.remove(sibling) else: self._nodesWithID[sibling._id] = sibling del self._nodesWithID[sibling._id] self._parent._children.remove(self) self._root = self self._parent = None elif not isinstance(parent, Node): ex = TypeError("Parameter 'parent' is not of type 'Node'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex else: if parent._root is self._root: raise AlreadyInTreeError(f"Parent '{parent}' is already a child node in this tree.") self._root = parent._root self._parent = parent self._level = parent._level + 1 for node in self.GetDescendants(): node._level = node._parent._level + 1 self._SetNewRoot(self._nodesWithID, self._nodesWithoutID) self._nodesWithID = self._nodesWithoutID = None parent._children.append(self) @readonly def Siblings(self) -> Tuple['Node', ...]: """ A read-only property to return a tuple of all siblings from the current node. If the current node is the only child, the tuple is empty. Siblings are child nodes of the current node's parent node, without the current node itself. :returns: A tuple of all siblings of the current node. :raises NoSiblingsError: If the current node has no parent node and thus no siblings. """ if self._parent is None: raise NoSiblingsError(f"Root node has no siblings.") return tuple([node for node in self._parent if node is not self]) @readonly def LeftSiblings(self) -> Tuple['Node', ...]: """ A read-only property to return a tuple of all siblings left from the current node. If the current node is the only child, the tuple is empty. Siblings are child nodes of the current node's parent node, without the current node itself. :returns: A tuple of all siblings left of the current node. :raises NoSiblingsError: If the current node has no parent node and thus no siblings. """ if self._parent is None: raise NoSiblingsError(f"Root node has no siblings.") result = [] for node in self._parent: if node is not self: result.append(node) else: break else: raise InternalError(f"Data structure corruption: Self is not part of parent's children.") # pragma: no cover return tuple(result) @readonly def RightSiblings(self) -> Tuple['Node', ...]: """ A read-only property to return a tuple of all siblings right from the current node. If the current node is the only child, the tuple is empty. Siblings are child nodes of the current node's parent node, without the current node itself. :returns: A tuple of all siblings right of the current node. :raises NoSiblingsError: If the current node has no parent node and thus no siblings. """ if self._parent is None: raise NoSiblingsError(f"Root node has no siblings.") result = [] iterator = iter(self._parent) for node in iterator: if node is self: break else: raise InternalError(f"Data structure corruption: Self is not part of parent's children.") # pragma: no cover for node in iterator: result.append(node) return tuple(result) def _GetPathAsLinkedList(self) -> Deque["Node"]: """ Compute the path from current node to root node by using a linked list (:class:`deque`). :meta private: :returns: Path from node to root node as double-ended queue (deque). """ path: Deque['Node'] = deque() node = self while node is not None: path.appendleft(node) node = node._parent return path @readonly def Path(self) -> Tuple['Node']: """ Read-only property to return the path from root node to the node as a tuple of nodes. :returns: A tuple of nodes describing the path from root node to the node. """ return tuple(self._GetPathAsLinkedList()) @readonly def Level(self) -> int: """ Read-only property to return a node's level in the tree. The level is the distance to the root node. :returns: The node's level. """ return self._level @readonly def Size(self) -> int: """ Read-only property to return the size of the tree. :returns: Count of all nodes in the tree structure. """ return len(self._root._nodesWithID) + len(self._root._nodesWithoutID) @readonly def IsRoot(self) -> bool: """ Returns true, if the node is the root node (representative node of the tree). :returns: ``True``, if node is the root node. """ return self._parent is None @readonly def IsLeaf(self) -> bool: """ Returns true, if the node is a leaf node (has no children). :returns: ``True``, if node has no children. """ return len(self._children) == 0 @readonly def HasChildren(self) -> bool: """ Returns true, if the node has child nodes. :returns: ``True``, if node has children. """ return len(self._children) > 0 def _SetNewRoot(self, nodesWithIDs: Dict['Node', 'Node'], nodesWithoutIDs: List['Node']) -> None: for nodeID, node in nodesWithIDs.items(): if nodeID in self._root._nodesWithID: raise ValueError(f"ID '{nodeID}' already exists in this tree.") else: self._root._nodesWithID[nodeID] = node node._root = self._root for node in nodesWithoutIDs: self._root._nodesWithoutID.append(node) node._root = self._root def AddChild(self, child: 'Node') -> None: """ Add a child node to the current node of the tree. If ``child`` is a subtree, both trees get merged. So all nodes in ``child`` get a new :attr:`_root` assigned and all IDs are merged into the node's root's ID lists (:attr:`_nodesWithID`). :param child: The child node to be added to the tree. :raises TypeError: If parameter ``child`` is not a :class:`Node`. :raises AlreadyInTreeError: If parameter ``child`` is already a node in the tree. .. seealso:: :attr:`Parent` |br| |rarr| Set the parent of a node. :meth:`AddChildren` |br| |rarr| Add multiple children at once. """ if not isinstance(child, Node): ex = TypeError(f"Parameter 'child' is not of type 'Node'.") ex.add_note(f"Got type '{getFullyQualifiedName(child)}'.") raise ex if child._root is self._root: raise AlreadyInTreeError(f"Child '{child}' is already a node in this tree.") child._root = self._root child._parent = self child._level = self._level + 1 for node in child.GetDescendants(): node._level = node._parent._level + 1 self._SetNewRoot(child._nodesWithID, child._nodesWithoutID) child._nodesWithID = child._nodesWithoutID = None self._children.append(child) def AddChildren(self, children: Iterable['Node']) -> None: """ Add multiple children nodes to the current node of the tree. :param children: The list of children nodes to be added to the tree. :raises TypeError: If parameter ``children`` contains an item, which is not a :class:`Node`. :raises AlreadyInTreeError: If parameter ``children`` contains an item, which is already a node in the tree. .. seealso:: :attr:`Parent` |br| |rarr| Set the parent of a node. :meth:`AddChild` |br| |rarr| Add a child node to the tree. """ for child in children: if not isinstance(child, Node): ex = TypeError(f"Item '{child}' in parameter 'children' is not of type 'Node'.") ex.add_note(f"Got type '{getFullyQualifiedName(child)}'.") raise ex if child._root is self._root: # TODO: create a more specific exception raise AlreadyInTreeError(f"Child '{child}' is already a node in this tree.") child._root = self._root child._parent = self child._level = self._level + 1 for node in child.GetDescendants(): node._level = node._parent._level + 1 self._SetNewRoot(child._nodesWithID, child._nodesWithoutID) child._nodesWithID = child._nodesWithoutID = None self._children.append(child) def GetPath(self) -> Generator['Node', None, None]: """ .. todo:: TREE::Node::GetPAth Needs documentation. """ for node in self._GetPathAsLinkedList(): yield node def GetAncestors(self) -> Generator['Node', None, None]: """ .. todo:: TREE::Node::GetAncestors Needs documentation. """ node = self._parent while node is not None: yield node node = node._parent def GetCommonAncestors(self, others: Union['Node', Iterable['Node']]) -> Generator['Node', None, None]: """ .. todo:: TREE::Node::GetCommonAncestors Needs documentation. """ if isinstance(others, Node): # Check for trivial case if others is self: for node in self._GetPathAsLinkedList(): yield node return # Check if both are in the same tree. if self._root is not others._root: raise NotInSameTreeError(f"Node 'others' is not in the same tree.") # Compute paths top-down and walk both paths until they deviate for left, right in zip(self.Path, others.Path): if left is right: yield left else: return elif isinstance(others, Iterable): raise NotImplementedError(f"Generator 'GetCommonAncestors' does not yet support an iterable of siblings to compute the common ancestors.") def GetChildren(self) -> Generator['Node', None, None]: """ A generator to iterate all direct children of the current node. :returns: A generator to iterate all children. .. seealso:: :meth:`GetDescendants` |br| |rarr| Iterate all descendants. :meth:`IterateLevelOrder` |br| |rarr| Iterate items level-by-level, which includes the node itself as a first returned node. :meth:`IteratePreOrder` |br| |rarr| Iterate items in pre-order, which includes the node itself as a first returned node. :meth:`IteratePostOrder` |br| |rarr| Iterate items in post-order, which includes the node itself as a last returned node. """ for child in self._children: yield child def GetSiblings(self) -> Generator['Node', None, None]: """ A generator to iterate all siblings. Siblings are child nodes of the current node's parent node, without the current node itself. :returns: A generator to iterate all siblings of the current node. :raises NoSiblingsError: If the current node has no parent node and thus no siblings. """ if self._parent is None: raise NoSiblingsError(f"Root node has no siblings.") for node in self._parent: if node is self: continue yield node def GetLeftSiblings(self) -> Generator['Node', None, None]: """ A generator to iterate all siblings left from the current node. Siblings are child nodes of the current node's parent node, without the current node itself. :returns: A generator to iterate all siblings left of the current node. :raises NoSiblingsError: If the current node has no parent node and thus no siblings. """ if self._parent is None: raise NoSiblingsError(f"Root node has no siblings.") for node in self._parent: if node is self: break yield node else: raise InternalError(f"Data structure corruption: Self is not part of parent's children.") # pragma: no cover def GetRightSiblings(self) -> Generator['Node', None, None]: """ A generator to iterate all siblings right from the current node. Siblings are child nodes of the current node's parent node, without the current node itself. :returns: A generator to iterate all siblings right of the current node. :raises NoSiblingsError: If the current node has no parent node and thus no siblings. """ if self._parent is None: raise NoSiblingsError(f"Root node has no siblings.") iterator = iter(self._parent) for node in iterator: if node is self: break else: raise InternalError(f"Data structure corruption: Self is not part of parent's children.") # pragma: no cover for node in iterator: yield node def GetDescendants(self) -> Generator['Node', None, None]: """ A generator to iterate all descendants of the current node. In contrast to `IteratePreOrder` and `IteratePostOrder` it doesn't include the node itself. :returns: A generator to iterate all descendants. .. seealso:: :meth:`GetChildren` |br| |rarr| Iterate all children, but no grand-children. :meth:`IterateLevelOrder` |br| |rarr| Iterate items level-by-level, which includes the node itself as a first returned node. :meth:`IteratePreOrder` |br| |rarr| Iterate items in pre-order, which includes the node itself as a first returned node. :meth:`IteratePostOrder` |br| |rarr| Iterate items in post-order, which includes the node itself as a last returned node. """ for child in self._children: yield child yield from child.GetDescendants() def GetRelatives(self) -> Generator['Node', None, None]: """ A generator to iterate all relatives (all siblings and all their descendants) of the current node. :returns: A generator to iterate all relatives. """ for node in self.GetSiblings(): yield node yield from node.GetDescendants() def GetLeftRelatives(self) -> Generator['Node', None, None]: """ A generator to iterate all left relatives (left siblings and all their descendants) of the current node. :returns: A generator to iterate all left relatives. """ for node in self.GetLeftSiblings(): yield node yield from node.GetDescendants() def GetRightRelatives(self) -> Generator['Node', None, None]: """ A generator to iterate all right relatives (right siblings and all their descendants) of the current node. :returns: A generator to iterate all right relatives. """ for node in self.GetRightSiblings(): yield node yield from node.GetDescendants() def IterateLeafs(self) -> Generator['Node', None, None]: """ A generator to iterate all leaf-nodes in a subtree, which subtree root is the current node. :returns: A generator to iterate leaf-nodes reachable from current node. """ for child in self._children: if child.IsLeaf: yield child else: yield from child.IterateLeafs() def IterateLevelOrder(self) -> Generator['Node', None, None]: """ A generator to iterate all siblings of the current node level-by-level top-down. In contrast to `GetDescendants`, this includes also the node itself as the first returned node. :returns: A generator to iterate all siblings level-by-level. .. seealso:: :meth:`GetChildren` |br| |rarr| Iterate all children, but no grand-children. :meth:`GetDescendants` |br| |rarr| Iterate all descendants. :meth:`IteratePreOrder` |br| |rarr| Iterate items in pre-order, which includes the node itself as a first returned node. :meth:`IteratePostOrder` |br| |rarr| Iterate items in post-order, which includes the node itself as a last returned node. """ queue = deque([self]) while queue: currentNode = queue.pop() yield currentNode for node in currentNode._children: queue.appendleft(node) def IteratePreOrder(self) -> Generator['Node', None, None]: """ A generator to iterate all siblings of the current node in pre-order. In contrast to `GetDescendants`, this includes also the node itself as the first returned node. :returns: A generator to iterate all siblings in pre-order. .. seealso:: :meth:`GetChildren` |br| |rarr| Iterate all children, but no grand-children. :meth:`GetDescendants` |br| |rarr| Iterate all descendants. :meth:`IterateLevelOrder` |br| |rarr| Iterate items level-by-level, which includes the node itself as a first returned node. :meth:`IteratePostOrder` |br| |rarr| Iterate items in post-order, which includes the node itself as a last returned node. """ yield self for child in self._children: yield from child.IteratePreOrder() def IteratePostOrder(self) -> Generator['Node', None, None]: """ A generator to iterate all siblings of the current node in post-order. In contrast to `GetDescendants`, this includes also the node itself as the last returned node. :returns: A generator to iterate all siblings in post-order. .. seealso:: :meth:`GetChildren` |br| |rarr| Iterate all children, but no grand-children. :meth:`GetDescendants` |br| |rarr| Iterate all descendants. :meth:`IterateLevelOrder` |br| |rarr| Iterate items level-by-level, which includes the node itself as a first returned node. :meth:`IteratePreOrder` |br| |rarr| Iterate items in pre-order, which includes the node itself as a first returned node. """ for child in self._children: yield from child.IteratePostOrder() yield self def WalkTo(self, other: 'Node') -> Generator['Node', None, None]: """ Returns a generator to iterate the path from node to another node. :param other: Node to walk to. :returns: Generator to iterate the path from node to other node. :raises NotInSameTreeError: If parameter ``other`` is not part of the same tree. """ # Check for trivial case if other is self: yield from () # Check if both are in the same tree. if self._root is not other._root: raise NotInSameTreeError(f"Node 'other' is not in the same tree.") # Compute both paths to the root. # 1. Walk from self to root, until a first common ancestor is found. # 2. Walk from there to other (reverse paths) otherPath = other.Path # TODO: Path generates a list and a tuple. Provide a generator for such a walk. index = len(otherPath) for node in self.GetAncestors(): try: index = otherPath.index(node) break except ValueError: yield node for i in range(index, len(otherPath)): yield otherPath[i] def GetNodeByID(self, nodeID: IDType) -> 'Node': """ Lookup a node by its unique ID. :param nodeID: ID of a node to lookup in the tree. :returns: Node for the given ID. :raises ValueError: If parameter ``nodeID`` is None. :raises KeyError: If parameter ``nodeID`` is not found in the tree. """ if nodeID is None: raise ValueError(f"'None' is not supported as an ID value.") return self._root._nodesWithID[nodeID] def Find(self, predicate: Callable) -> Generator['Node', None, None]: raise NotImplementedError(f"Method 'Find' is not yet implemented.") def __iter__(self) -> Iterator['Node']: """ Returns an iterator to iterate all child nodes. :returns: Children iterator. """ return iter(self._children) def __len__(self) -> int: """ Returns the number of children, but not including grand-children. :returns: Number of child nodes. """ return len(self._children) def __repr__(self) -> str: """ Returns a detailed string representation of the node. :returns: The detailed string representation of the node. """ nodeID = parent = value = "" if self._id is not None: nodeID = f"; nodeID='{self._id}'" if (self._parent is not None) and (self._parent._id is not None): parent = f"; parent='{self._parent._id}'" if self._value is not None: value = f"; value='{self._value}'" return f"" def __str__(self) -> str: """ Return a string representation of the node. Order of resolution: 1. If :attr:`_value` is not None, return the string representation of :attr:`_value`. 2. If :attr:`_id` is not None, return the string representation of :attr:`_id`. 3. Else, return :meth:`__repr__`. :returns: The resolved string representation of the node. """ if self._value is not None: return str(self._value) elif self._id is not None: return str(self._id) else: return self.__repr__() def Render( self, prefix: str = "", lineend: str = "\n", nodeMarker: str = "├─", lastNodeMarker: str = "└─", bypassMarker: str = "│ " ) -> str: """ Render the tree as ASCII art. :param prefix: A string printed in front of every line, e.g. for indentation. Default: ``""``. :param lineend: A string printed at the end of every line. Default: ``"\\n"``. :param nodeMarker: A string printed before every non-last tree node. Default: ``"├─"``. :param lastNodeMarker: A string printed before every last tree node. Default: ``"└─"``. :param bypassMarker: A string printed when there are further nodes in the parent level. Default: ``"│ "``. :return: A rendered tree as multiline string. """ emptyMarker = " " * len(bypassMarker) def _render(node: Node, markers: str): result = [] if node.HasChildren: for child in node._children[:-1]: nodeRepresentation = child._format(child) if child._format else str(child) result.append(f"{prefix}{markers}{nodeMarker}{nodeRepresentation}{lineend}") result.extend(_render(child, markers + bypassMarker)) # last child node child = node._children[-1] nodeRepresentation = child._format(child) if child._format else str(child) result.append(f"{prefix}{markers}{lastNodeMarker}{nodeRepresentation}{lineend}") result.extend(_render(child, markers + emptyMarker)) return result # Root element nodeRepresentation = self._format(self) if self._format else str(self) result = [f"{prefix}{nodeRepresentation}{lineend}"] result.extend(_render(self, "")) return "".join(result) pyTooling-8.11.0/pyTooling/Versioning/000077500000000000000000000000001513317154500177015ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Versioning/__init__.py000066400000000000000000002571671513317154500220340ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2020-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Implementation of semantic and date versioning version-numbers. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from collections.abc import Iterable as abc_Iterable from enum import Flag, Enum from re import compile as re_compile from typing import Optional as Nullable, Union, Callable, Any, Generic, TypeVar, Iterable, Iterator, List try: from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride from pyTooling.Exceptions import ToolingException from pyTooling.Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError): # pragma: no cover print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from MetaClasses import ExtendedType, abstractmethod, mustoverride from Exceptions import ToolingException from Common import getFullyQualifiedName except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover print("[pyTooling.Versioning] Could not import directly!") raise ex @export class Parts(Flag): """Enumeration describing parts of a version number that can be present.""" Unknown = 0 #: Undocumented Major = 1 #: Major number is present. (e.g. X in ``vX.0.0``). Year = 1 #: Year is present. (e.g. X in ``XXXX.10``). Minor = 2 #: Minor number is present. (e.g. Y in ``v0.Y.0``). Month = 2 #: Month is present. (e.g. X in ``2024.YY``). Week = 2 #: Week is present. (e.g. X in ``2024.YY``). Micro = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``). Patch = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``). Day = 4 #: Day is present. (e.g. X in ``2024.10.ZZ``). Level = 8 #: Release level is present. Dev = 16 #: Development part is present. Build = 32 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``) Post = 64 #: Post-release number is present. Prefix = 128 #: Prefix is present. Postfix = 256 #: Postfix is present. Hash = 512 #: Hash is present. # AHead = 256 @export class ReleaseLevel(Enum): """Enumeration describing the version's maturity level.""" Final = 0 #: ReleaseCandidate = -10 #: Development = -20 #: Gamma = -30 #: Beta = -40 #: Alpha = -50 #: def __eq__(self, other: Any) -> bool: """ Compare two release levels if the level is equal to the second operand. :param other: Operand to compare against. :returns: ``True``, if release level is equal the second operand's release level. :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`. """ if isinstance(other, str): other = ReleaseLevel(other) if not isinstance(other, ReleaseLevel): ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.") raise ex return self is other def __ne__(self, other: Any) -> bool: """ Compare two release levels if the level is unequal to the second operand. :param other: Operand to compare against. :returns: ``True``, if release level is unequal the second operand's release level. :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`. """ if isinstance(other, str): other = ReleaseLevel(other) if not isinstance(other, ReleaseLevel): ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by != operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.") raise ex return self is not other def __lt__(self, other: Any) -> bool: """ Compare two release levels if the level is less than the second operand. :param other: Operand to compare against. :returns: ``True``, if release level is less than the second operand. :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`. """ if isinstance(other, str): other = ReleaseLevel(other) if not isinstance(other, ReleaseLevel): ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.") raise ex return self.value < other.value def __le__(self, other: Any) -> bool: """ Compare two release levels if the level is less than or equal the second operand. :param other: Operand to compare against. :returns: ``True``, if release level is less than or equal the second operand. :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`. """ if isinstance(other, str): other = ReleaseLevel(other) if not isinstance(other, ReleaseLevel): ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <=>= operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.") raise ex return self.value <= other.value def __gt__(self, other: Any) -> bool: """ Compare two release levels if the level is greater than the second operand. :param other: Operand to compare against. :returns: ``True``, if release level is greater than the second operand. :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`. """ if isinstance(other, str): other = ReleaseLevel(other) if not isinstance(other, ReleaseLevel): ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.") raise ex return self.value > other.value def __ge__(self, other: Any) -> bool: """ Compare two release levels if the level is greater than or equal the second operand. :param other: Operand to compare against. :returns: ``True``, if release level is greater than or equal the second operand. :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`. """ if isinstance(other, str): other = ReleaseLevel(other) if not isinstance(other, ReleaseLevel): ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.") raise ex return self.value >= other.value def __hash__(self) -> int: return hash(self.value) def __str__(self) -> str: """ Returns the release level's string equivalent. :returns: The string equivalent of the release level. """ if self is ReleaseLevel.Final: return "final" elif self is ReleaseLevel.ReleaseCandidate: return "rc" elif self is ReleaseLevel.Development: return "dev" elif self is ReleaseLevel.Beta: return "beta" elif self is ReleaseLevel.Alpha: return "alpha" raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.") @export class Flags(Flag): """State enumeration, if a (tagged) version is build from a clean or dirty working directory.""" NoVCS = 0 #: No Version Control System VCS Clean = 1 #: A versioned build was created from a *clean* working directory. Dirty = 2 #: A versioned build was created from a *dirty* working directory. CVS = 16 #: Concurrent Versions System (CVS) SVN = 32 #: Subversion (SVN) Git = 64 #: Git Hg = 128 #: Mercurial (Hg) @export def WordSizeValidator( bits: Nullable[int] = None, majorBits: Nullable[int] = None, minorBits: Nullable[int] = None, microBits: Nullable[int] = None, buildBits: Nullable[int] = None ): """ A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits. :param bits: Number of bits to encode any positive version number part. :param majorBits: Number of bits to encode a positive major number in a version. :param minorBits: Number of bits to encode a positive minor number in a version. :param microBits: Number of bits to encode a positive micro number in a version. :param buildBits: Number of bits to encode a positive build number in a version. :return: A validation function for Version instances. """ majorMax = minorMax = microMax = buildMax = -1 if bits is not None: majorMax = minorMax = microMax = buildMax = 2**bits - 1 if majorBits is not None: majorMax = 2**majorBits - 1 if minorBits is not None: minorMax = 2**minorBits - 1 if microBits is not None: microMax = 2 ** microBits - 1 if buildBits is not None: buildMax = 2**buildBits - 1 def validator(version: SemanticVersion) -> bool: if Parts.Major in version._parts and version._major > majorMax: raise ValueError(f"Field 'Version.Major' > {majorMax}.") if Parts.Minor in version._parts and version._minor > minorMax: raise ValueError(f"Field 'Version.Minor' > {minorMax}.") if Parts.Micro in version._parts and version._micro > microMax: raise ValueError(f"Field 'Version.Micro' > {microMax}.") if Parts.Build in version._parts and version._build > buildMax: raise ValueError(f"Field 'Version.Build' > {buildMax}.") return True return validator @export def MaxValueValidator( max: Nullable[int] = None, majorMax: Nullable[int] = None, minorMax: Nullable[int] = None, microMax: Nullable[int] = None, buildMax: Nullable[int] = None ): """ A factory function to return a validator for Version instances checking for a positive integer range [0..max]. :param max: The upper bound for any positive version number part. :param majorMax: The upper bound for the positive major number. :param minorMax: The upper bound for the positive minor number. :param microMax: The upper bound for the positive micro number. :param buildMax: The upper bound for the positive build number. :return: A validation function for Version instances. """ if max is not None: majorMax = minorMax = microMax = buildMax = max def validator(version: SemanticVersion) -> bool: if Parts.Major in version._parts and version._major > majorMax: raise ValueError(f"Field 'Version.Major' > {majorMax}.") if Parts.Minor in version._parts and version._minor > minorMax: raise ValueError(f"Field 'Version.Minor' > {minorMax}.") if Parts.Micro in version._parts and version._micro > microMax: raise ValueError(f"Field 'Version.Micro' > {microMax}.") if Parts.Build in version._parts and version._build > buildMax: raise ValueError(f"Field 'Version.Build' > {buildMax}.") return True return validator @export class Version(metaclass=ExtendedType, slots=True): """Base-class for a version representation.""" __hash: Nullable[int] #: once computed hash of the object _parts: Parts #: Integer flag enumeration of present parts in a version number. _prefix: str #: Prefix string _major: int #: Major number part of the version number. _minor: int #: Minor number part of the version number. _micro: int #: Micro number part of the version number. _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...). _releaseNumber: int #: Release number (Python calls this a serial). _post: int #: Post-release version number part. _dev: int #: Development number _build: int #: Build number part of the version number. _postfix: str #: Postfix string _hash: str #: Hash from version control system. _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version. def __init__( self, major: int, minor: Nullable[int] = None, micro: Nullable[int] = None, level: Nullable[ReleaseLevel] = ReleaseLevel.Final, number: Nullable[int] = None, post: Nullable[int] = None, dev: Nullable[int] = None, *, build: Nullable[int] = None, postfix: Nullable[str] = None, prefix: Nullable[str] = None, hash: Nullable[str] = None, flags: Flags = Flags.NoVCS ) -> None: """ Initializes a version number representation. :param major: Major number part of the version number. :param minor: Minor number part of the version number. :param micro: Micro (patch) number part of the version number. :param level: Release level (alpha, beta, release candidate, final, ...) of the version number. :param number: Release number part (in combination with release level) of the version number. :param post: Post number part of the version number. :param dev: Development number part of the version number. :param build: Build number part of the version number. :param postfix: The version number's postfix. :param prefix: The version number's prefix. :param hash: Postfix string. :param flags: The version number's flags. :raises TypeError: If parameter 'major' is not of type int. :raises ValueError: If parameter 'major' is a negative number. :raises TypeError: If parameter 'minor' is not of type int. :raises ValueError: If parameter 'minor' is a negative number. :raises TypeError: If parameter 'micro' is not of type int. :raises ValueError: If parameter 'micro' is a negative number. :raises TypeError: If parameter 'build' is not of type int. :raises ValueError: If parameter 'build' is a negative number. :raises TypeError: If parameter 'prefix' is not of type str. :raises TypeError: If parameter 'postfix' is not of type str. """ self.__hash = None if not isinstance(major, int): raise TypeError("Parameter 'major' is not of type 'int'.") elif major < 0: raise ValueError("Parameter 'major' is negative.") self._parts = Parts.Major self._major = major if minor is not None: if not isinstance(minor, int): raise TypeError("Parameter 'minor' is not of type 'int'.") elif minor < 0: raise ValueError("Parameter 'minor' is negative.") self._parts |= Parts.Minor self._minor = minor else: self._minor = 0 if micro is not None: if not isinstance(micro, int): raise TypeError("Parameter 'micro' is not of type 'int'.") elif micro < 0: raise ValueError("Parameter 'micro' is negative.") self._parts |= Parts.Micro self._micro = micro else: self._micro = 0 if level is None: raise ValueError("Parameter 'level' is None.") elif not isinstance(level, ReleaseLevel): raise TypeError("Parameter 'level' is not of type 'ReleaseLevel'.") elif level is ReleaseLevel.Final: if number is not None: raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.") self._parts |= Parts.Level self._releaseLevel = level self._releaseNumber = 0 else: self._parts |= Parts.Level self._releaseLevel = level if number is not None: if not isinstance(number, int): raise TypeError("Parameter 'number' is not of type 'int'.") elif number < 0: raise ValueError("Parameter 'number' is negative.") self._releaseNumber = number else: self._releaseNumber = 0 if dev is not None: if not isinstance(dev, int): raise TypeError("Parameter 'dev' is not of type 'int'.") elif dev < 0: raise ValueError("Parameter 'dev' is negative.") self._parts |= Parts.Dev self._dev = dev else: self._dev = 0 if post is not None: if not isinstance(post, int): raise TypeError("Parameter 'post' is not of type 'int'.") elif post < 0: raise ValueError("Parameter 'post' is negative.") self._parts |= Parts.Post self._post = post else: self._post = 0 if build is not None: if not isinstance(build, int): raise TypeError("Parameter 'build' is not of type 'int'.") elif build < 0: raise ValueError("Parameter 'build' is negative.") self._build = build self._parts |= Parts.Build else: self._build = 0 if postfix is not None: if not isinstance(postfix, str): raise TypeError("Parameter 'postfix' is not of type 'str'.") self._parts |= Parts.Postfix self._postfix = postfix else: self._postfix = "" if prefix is not None: if not isinstance(prefix, str): raise TypeError("Parameter 'prefix' is not of type 'str'.") self._parts |= Parts.Prefix self._prefix = prefix else: self._prefix = "" if hash is not None: if not isinstance(hash, str): raise TypeError("Parameter 'hash' is not of type 'str'.") self._parts |= Parts.Hash self._hash = hash else: self._hash = "" if flags is None: raise ValueError("Parameter 'flags' is None.") elif not isinstance(flags, Flags): raise TypeError("Parameter 'flags' is not of type 'Flags'.") self._flags = flags @classmethod @abstractmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "Version": """Parse a version string and return a Version instance.""" @readonly def Parts(self) -> Parts: """ Read-only property to access the used parts of this version number. :return: A flag enumeration of used version number parts. """ return self._parts @readonly def Prefix(self) -> str: """ Read-only property to access the version number's prefix. :return: The prefix of the version number. """ return self._prefix @readonly def Major(self) -> int: """ Read-only property to access the major number. :return: The major number. """ return self._major @readonly def Minor(self) -> int: """ Read-only property to access the minor number. :return: The minor number. """ return self._minor @readonly def Micro(self) -> int: """ Read-only property to access the micro number. :return: The micro number. """ return self._micro @readonly def ReleaseLevel(self) -> ReleaseLevel: """ Read-only property to access the release level. :return: The release level. """ return self._releaseLevel @readonly def ReleaseNumber(self) -> int: """ Read-only property to access the release number. :return: The release number. """ return self._releaseNumber @readonly def Post(self) -> int: """ Read-only property to access the post number. :return: The post number. """ return self._post @readonly def Dev(self) -> int: """ Read-only property to access the development number. :return: The development number. """ return self._dev @readonly def Build(self) -> int: """ Read-only property to access the build number. :return: The build number. """ return self._build @readonly def Postfix(self) -> str: """ Read-only property to access the version number's postfix. :return: The postfix of the version number. """ return self._postfix @readonly def Hash(self) -> str: """ Read-only property to access the version number's hash. :return: The hash. """ return self._hash @readonly def Flags(self) -> Flags: """ Read-only property to access the version number's flags. :return: The flags of the version number. """ return self._flags def _equal(self, left: "Version", right: "Version") -> Nullable[bool]: """ Private helper method to compute the equality of two :class:`Version` instances. :param left: Left operand. :param right: Right operand. :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``. """ return ( (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro) and (left._releaseLevel == right._releaseLevel) and (left._releaseNumber == right._releaseNumber) and (left._post == right._post) and (left._dev == right._dev) and (left._build == right._build) and (left._postfix == right._postfix) ) def _compare(self, left: "Version", right: "Version") -> Nullable[bool]: """ Private helper method to compute the comparison of two :class:`Version` instances. :param left: Left operand. :param right: Right operand. :returns: ``True``, if ``left`` is smaller than ``right``. |br| False if ``left`` is greater than ``right``. |br| Otherwise it's None (both operands are equal). """ if left._major < right._major: return True elif left._major > right._major: return False if left._minor < right._minor: return True elif left._minor > right._minor: return False if left._micro < right._micro: return True elif left._micro > right._micro: return False if left._releaseLevel < right._releaseLevel: return True elif left._releaseLevel > right._releaseLevel: return False if left._releaseNumber < right._releaseNumber: return True elif left._releaseNumber > right._releaseNumber: return False if left._post < right._post: return True elif left._post > right._post: return False if left._dev < right._dev: return True elif left._dev > right._dev: return False if left._build < right._build: return True elif left._build > right._build: return False return None def _minimum(self, actual: "Version", expected: "Version") -> Nullable[bool]: exactMajor = Parts.Minor in expected._parts exactMinor = Parts.Micro in expected._parts if exactMajor and actual._major != expected._major: return False elif not exactMajor and actual._major < expected._major: return False if exactMinor and actual._minor != expected._minor: return False elif not exactMinor and actual._minor < expected._minor: return False if Parts.Micro in expected._parts: return actual._micro >= expected._micro return True def _format(self, formatSpec: str) -> str: """ Return a string representation of this version number according to the format specification. .. topic:: Format Specifiers * ``%p`` - prefix * ``%M`` - major number * ``%m`` - minor number * ``%u`` - micro number * ``%b`` - build number :param formatSpec: The format specification. :return: Formatted version number. """ if formatSpec == "": return self.__str__() result = formatSpec result = result.replace("%p", str(self._prefix)) result = result.replace("%M", str(self._major)) result = result.replace("%m", str(self._minor)) result = result.replace("%u", str(self._micro)) result = result.replace("%b", str(self._build)) result = result.replace("%r", str(self._releaseLevel)[0]) result = result.replace("%R", str(self._releaseLevel)) result = result.replace("%n", str(self._releaseNumber)) result = result.replace("%d", str(self._dev)) result = result.replace("%P", str(self._postfix)) return result @mustoverride def __eq__(self, other: Union["Version", str, int, None]) -> bool: """ Compare two version numbers for equality. The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if both version numbers are equal. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`. """ if other is None: raise ValueError(f"Second operand is None.") elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): pass elif isinstance(other, str): other = self.__class__.Parse(other) elif isinstance(other, int): other = self.__class__(major=other) else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int") raise ex return self._equal(self, other) @mustoverride def __ne__(self, other: Union["Version", str, int, None]) -> bool: """ Compare two version numbers for inequality. The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if both version numbers are not equal. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`. """ if other is None: raise ValueError(f"Second operand is None.") elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): pass elif isinstance(other, str): other = self.__class__.Parse(other) elif isinstance(other, int): other = self.__class__(major=other) else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int") raise ex return not self._equal(self, other) @mustoverride def __lt__(self, other: Union["Version", str, int, None]) -> bool: """ Compare two version numbers if the version is less than the second operand. The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`, ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if version is less than the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`. """ if other is None: raise ValueError(f"Second operand is None.") elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): pass elif isinstance(other, VersionRange): other = other._lowerBound elif isinstance(other, VersionSet): other = other._items[0] elif isinstance(other, str): other = self.__class__.Parse(other) elif isinstance(other, int): other = self.__class__(major=other) else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int") raise ex return self._compare(self, other) is True @mustoverride def __le__(self, other: Union["Version", str, int, None]) -> bool: """ Compare two version numbers if the version is less than or equal the second operand. The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if version is less than or equal the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`. """ equalValue = True if other is None: raise ValueError(f"Second operand is None.") elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): pass elif isinstance(other, VersionRange): equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling other = other._lowerBound elif isinstance(other, VersionSet): other = other._items[0] elif isinstance(other, str): other = self.__class__.Parse(other) elif isinstance(other, int): other = self.__class__(major=other) else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int") raise ex result = self._compare(self, other) return result if result is not None else equalValue @mustoverride def __gt__(self, other: Union["Version", str, int, None]) -> bool: """ Compare two version numbers if the version is greater than the second operand. The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if version is greater than the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`. """ if other is None: raise ValueError(f"Second operand is None.") elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): pass elif isinstance(other, VersionRange): other = other._upperBound elif isinstance(other, VersionSet): other = other._items[-1] elif isinstance(other, str): other = self.__class__.Parse(other) elif isinstance(other, int): other = self.__class__(major=other) else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int") raise ex return self._compare(self, other) is False @mustoverride def __ge__(self, other: Union["Version", str, int, None]) -> bool: """ Compare two version numbers if the version is greater than or equal the second operand. The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if version is greater than or equal the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`. """ equalValue = True if other is None: raise ValueError(f"Second operand is None.") elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): pass elif isinstance(other, VersionRange): equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling other = other._upperBound elif isinstance(other, VersionSet): other = other._items[-1] elif isinstance(other, str): other = self.__class__.Parse(other) elif isinstance(other, int): other = self.__class__(major=other) else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int") raise ex result = self._compare(self, other) return not result if result is not None else equalValue def __rshift__(self, other: Union["Version", str, int, None]) -> bool: if other is None: raise ValueError(f"Second operand is None.") elif isinstance(other, self.__class__): pass elif isinstance(other, str): other = self.__class__.Parse(other) elif isinstance(other, int): other = self.__class__(major=other) else: ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >> operator.") ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int") raise ex return self._minimum(self, other) def __hash__(self) -> int: if self.__hash is None: self.__hash = hash(( self._prefix, self._major, self._minor, self._micro, self._releaseLevel, self._releaseNumber, self._post, self._dev, self._build, self._postfix, self._hash, self._flags )) return self.__hash @export class SemanticVersion(Version): """Representation of a semantic version number like ``3.7.12``.""" _PATTERN = re_compile( r"^" r"(?P[a-zA-Z]*)" r"(?P\d+)" r"(?:\.(?P\d+))?" r"(?:\.(?P\d+))?" r"(?:" r"(?:\.(?P\d+))" r"|" r"(?:[-](?Pdev|final))" r"|" r"(?:(?P[\.\-]?)(?Palpha|beta|gamma|a|b|c|rc|pl)(?P\d+))" r")?" r"(?:(?P[\.\-]post)(?P\d+))?" r"(?:(?P[\.\-]dev)(?P\d+))?" r"(?:(?P[\.\-\+])(?P\w+))?" r"$" ) # QUESTION: was this how many commits a version is ahead of the last tagged version? # ahead: int = 0 def __init__( self, major: int, minor: Nullable[int] = None, micro: Nullable[int] = None, level: Nullable[ReleaseLevel] = ReleaseLevel.Final, number: Nullable[int] = None, post: Nullable[int] = None, dev: Nullable[int] = None, *, build: Nullable[int] = None, postfix: Nullable[str] = None, prefix: Nullable[str] = None, hash: Nullable[str] = None, flags: Flags = Flags.NoVCS ) -> None: """ Initializes a semantic version number representation. :param major: Major number part of the version number. :param minor: Minor number part of the version number. :param micro: Micro (patch) number part of the version number. :param build: Build number part of the version number. :param level: tbd :param number: tbd :param post: Post number part of the version number. :param dev: Development number part of the version number. :param prefix: The version number's prefix. :param postfix: The version number's postfix. :param flags: The version number's flags. :param hash: tbd :raises TypeError: If parameter 'major' is not of type int. :raises ValueError: If parameter 'major' is a negative number. :raises TypeError: If parameter 'minor' is not of type int. :raises ValueError: If parameter 'minor' is a negative number. :raises TypeError: If parameter 'micro' is not of type int. :raises ValueError: If parameter 'micro' is a negative number. :raises TypeError: If parameter 'build' is not of type int. :raises ValueError: If parameter 'build' is a negative number. :raises TypeError: If parameter 'post' is not of type int. :raises ValueError: If parameter 'post' is a negative number. :raises TypeError: If parameter 'dev' is not of type int. :raises ValueError: If parameter 'dev' is a negative number. :raises TypeError: If parameter 'prefix' is not of type str. :raises TypeError: If parameter 'postfix' is not of type str. """ super().__init__(major, minor, micro, level, number, post, dev, build=build, postfix=postfix, prefix=prefix, hash=hash, flags=flags) @classmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "SemanticVersion": """ Parse a version string and return a :class:`SemanticVersion` instance. Allowed prefix characters: * ``v|V`` - version, public version, public release * ``i|I`` - internal version, internal release * ``r|R`` - release, revision * ``rev|REV`` - revision :param versionString: The version string to parse. :param validator: Optional, a validation function. :returns: An object representing a semantic version. :raises TypeError: When parameter ``versionString`` is not a string. :raises ValueError: When parameter ``versionString`` is None. :raises ValueError: When parameter ``versionString`` is empty. """ if versionString is None: raise ValueError("Parameter 'versionString' is None.") elif not isinstance(versionString, str): ex = TypeError(f"Parameter 'versionString' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.") raise ex elif (versionString := versionString.strip()) == "": raise ValueError("Parameter 'versionString' is empty.") if (match := cls._PATTERN.match(versionString)) is None: raise ValueError(f"Syntax error in parameter 'versionString': '{versionString}'") def toInt(value: Nullable[str]) -> Nullable[int]: if value is None or value == "": return None try: return int(value) except ValueError as ex: # pragma: no cover raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex release = match["release"] if release is not None: if release == "dev": releaseLevel = ReleaseLevel.Development elif release == "final": releaseLevel = ReleaseLevel.Final else: # pragma: no cover raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.") else: level = match["level"] if level is not None: level = level.lower() if level == "a" or level == "alpha": releaseLevel = ReleaseLevel.Alpha elif level == "b" or level == "beta": releaseLevel = ReleaseLevel.Beta elif level == "c" or level == "gamma": releaseLevel = ReleaseLevel.Gamma elif level == "rc": releaseLevel = ReleaseLevel.ReleaseCandidate else: # pragma: no cover raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.") else: releaseLevel = ReleaseLevel.Final version = cls( major=toInt(match["major"]), minor=toInt(match["minor"]), micro=toInt(match["micro"]), level=releaseLevel, number=toInt(match["number"]), post=toInt(match["post"]), dev=toInt(match["dev"]), build=toInt(match["build"]), postfix=match["postfix"], prefix=match["prefix"], # hash=match["hash"], flags=Flags.Clean ) if validator is not None and not validator(version): # TODO: VersionValidatorException raise ValueError(f"Failed to validate version string '{versionString}'.") return version @readonly def Patch(self) -> int: """ Read-only property to access the patch number. The patch number is identical to the micro number. :return: The patch number. """ return self._micro def _equal(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]: """ Private helper method to compute the equality of two :class:`SemanticVersion` instances. :param left: Left operand. :param right: Right operand. :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``. """ return super()._equal(left, right) def _compare(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]: """ Private helper method to compute the comparison of two :class:`SemanticVersion` instances. :param left: Left operand. :param right: Right operand. :returns: ``True``, if ``left`` is smaller than ``right``. |br| False if ``left`` is greater than ``right``. |br| Otherwise it's None (both operands are equal). """ return super()._compare(left, right) def __eq__(self, other: Union["SemanticVersion", str, int, None]) -> bool: """ Compare two version numbers for equality. The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if both version numbers are equal. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`. """ return super().__eq__(other) def __ne__(self, other: Union["SemanticVersion", str, int, None]) -> bool: """ Compare two version numbers for inequality. The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if both version numbers are not equal. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`. """ return super().__ne__(other) def __lt__(self, other: Union["SemanticVersion", str, int, None]) -> bool: """ Compare two version numbers if the version is less than the second operand. The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if version is less than the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`. """ return super().__lt__(other) def __le__(self, other: Union["SemanticVersion", str, int, None]) -> bool: """ Compare two version numbers if the version is less than or equal the second operand. The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if version is less than or equal the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`. """ return super().__le__(other) def __gt__(self, other: Union["SemanticVersion", str, int, None]) -> bool: """ Compare two version numbers if the version is greater than the second operand. The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if version is greater than the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`. """ return super().__gt__(other) def __ge__(self, other: Union["SemanticVersion", str, int, None]) -> bool: """ Compare two version numbers if the version is greater than or equal the second operand. The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Operand to compare against. :returns: ``True``, if version is greater than or equal the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`. """ return super().__ge__(other) def __rshift__(self, other: Union["SemanticVersion", str, int, None]) -> bool: return super().__rshift__(other) def __hash__(self) -> int: return super().__hash__() def __format__(self, formatSpec: str) -> str: result = self._format(formatSpec) if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.") return result.replace("%%", "%") def __repr__(self) -> str: """ Return a string representation of this version number without prefix ``v``. :returns: Raw version number representation without a prefix. """ return f"{self._prefix if Parts.Prefix in self._parts else ''}{self._major}.{self._minor}.{self._micro}" def __str__(self) -> str: """ Return a string representation of this version number. :returns: Version number representation. """ result = self._prefix if Parts.Prefix in self._parts else "" result += f"{self._major}" # major is always present result += f".{self._minor}" if Parts.Minor in self._parts else "" result += f".{self._micro}" if Parts.Micro in self._parts else "" result += f".{self._build}" if Parts.Build in self._parts else "" if self._releaseLevel is ReleaseLevel.Development: result += "-dev" elif self._releaseLevel is ReleaseLevel.Alpha: result += f".alpha{self._releaseNumber}" elif self._releaseLevel is ReleaseLevel.Beta: result += f".beta{self._releaseNumber}" elif self._releaseLevel is ReleaseLevel.Gamma: result += f".gamma{self._releaseNumber}" elif self._releaseLevel is ReleaseLevel.ReleaseCandidate: result += f".rc{self._releaseNumber}" result += f".post{self._post}" if Parts.Post in self._parts else "" result += f".dev{self._dev}" if Parts.Dev in self._parts else "" result += f"+{self._postfix}" if Parts.Postfix in self._parts else "" return result @export class PythonVersion(SemanticVersion): """ Represents a Python version. """ @classmethod def FromSysVersionInfo(cls) -> "PythonVersion": """ Create a Python version from :data:`sys.version_info`. :returns: A PythonVersion instance of the current Python interpreter's version. """ from sys import version_info if version_info.releaselevel == "final": rl = ReleaseLevel.Final number = None else: # pragma: no cover number = version_info.serial if version_info.releaselevel == "alpha": rl = ReleaseLevel.Alpha elif version_info.releaselevel == "beta": rl = ReleaseLevel.Beta elif version_info.releaselevel == "candidate": rl = ReleaseLevel.ReleaseCandidate else: # pragma: no cover raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.") return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number) def __hash__(self) -> int: return super().__hash__() def __str__(self) -> str: """ Return a string representation of this version number. :returns: Version number representation. """ result = self._prefix if Parts.Prefix in self._parts else "" result += f"{self._major}" # major is always present result += f".{self._minor}" if Parts.Minor in self._parts else "" result += f".{self._micro}" if Parts.Micro in self._parts else "" if self._releaseLevel is ReleaseLevel.Alpha: result += f"a{self._releaseNumber}" elif self._releaseLevel is ReleaseLevel.Beta: result += f"b{self._releaseNumber}" elif self._releaseLevel is ReleaseLevel.Gamma: result += f"c{self._releaseNumber}" elif self._releaseLevel is ReleaseLevel.ReleaseCandidate: result += f"rc{self._releaseNumber}" result += f".post{self._post}" if Parts.Post in self._parts else "" result += f".dev{self._dev}" if Parts.Dev in self._parts else "" result += f"+{self._postfix}" if Parts.Postfix in self._parts else "" return result @export class CalendarVersion(Version): """Representation of a calendar version number like ``2021.10``.""" def __init__( self, major: int, minor: Nullable[int] = None, micro: Nullable[int] = None, build: Nullable[int] = None, flags: Flags = Flags.Clean, prefix: Nullable[str] = None, postfix: Nullable[str] = None ) -> None: """ Initializes a calendar version number representation. :param major: Major number part of the version number. :param minor: Minor number part of the version number. :param micro: Micro (patch) number part of the version number. :param build: Build number part of the version number. :param flags: The version number's flags. :param prefix: The version number's prefix. :param postfix: The version number's postfix. :raises TypeError: If parameter 'major' is not of type int. :raises ValueError: If parameter 'major' is a negative number. :raises TypeError: If parameter 'minor' is not of type int. :raises ValueError: If parameter 'minor' is a negative number. :raises TypeError: If parameter 'micro' is not of type int. :raises ValueError: If parameter 'micro' is a negative number. :raises TypeError: If parameter 'build' is not of type int. :raises ValueError: If parameter 'build' is a negative number. :raises TypeError: If parameter 'prefix' is not of type str. :raises TypeError: If parameter 'postfix' is not of type str. """ super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags) @classmethod def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["CalendarVersion"], bool]] = None) -> "CalendarVersion": """ Parse a version string and return a :class:`CalendarVersion` instance. :param versionString: The version string to parse. :returns: An object representing a calendar version. :raises TypeError: If parameter ``other`` is not a string. :raises ValueError: If parameter ``other`` is None. :raises ValueError: If parameter ``other`` is empty. """ parts = Parts.Unknown if versionString is None: raise ValueError("Parameter 'versionString' is None.") elif not isinstance(versionString, str): ex = TypeError(f"Parameter 'versionString' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.") raise ex elif versionString == "": raise ValueError("Parameter 'versionString' is empty.") split = versionString.split(".") length = len(split) major = int(split[0]) minor = 0 parts |= Parts.Major if length >= 2: minor = int(split[1]) parts |= Parts.Minor flags = Flags.Clean version = cls(major, minor, flags=flags) if validator is not None and not validator(version): raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover return version @property def Year(self) -> int: """ Read-only property to access the year part. :return: The year part. """ return self._major def _equal(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]: """ Private helper method to compute the equality of two :class:`CalendarVersion` instances. :param left: Left parameter. :param right: Right parameter. :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``. """ return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro) def _compare(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]: """ Private helper method to compute the comparison of two :class:`CalendarVersion` instances. :param left: Left parameter. :param right: Right parameter. :returns: ``True``, if ``left`` is smaller than ``right``. |br| False if ``left`` is greater than ``right``. |br| Otherwise it's None (both parameters are equal). """ if left._major < right._major: return True elif left._major > right._major: return False if left._minor < right._minor: return True elif left._minor > right._minor: return False if left._micro < right._micro: return True elif left._micro > right._micro: return False return None def __eq__(self, other: Union["CalendarVersion", str, int, None]) -> bool: """ Compare two version numbers for equality. The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Parameter to compare against. :returns: ``True``, if both version numbers are equal. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`. """ return super().__eq__(other) def __ne__(self, other: Union["CalendarVersion", str, int, None]) -> bool: """ Compare two version numbers for inequality. The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Parameter to compare against. :returns: ``True``, if both version numbers are not equal. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`. """ return super().__ne__(other) def __lt__(self, other: Union["CalendarVersion", str, int, None]) -> bool: """ Compare two version numbers if the version is less than the second operand. The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Parameter to compare against. :returns: ``True``, if version is less than the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`. """ return super().__lt__(other) def __le__(self, other: Union["CalendarVersion", str, int, None]) -> bool: """ Compare two version numbers if the version is less than or equal the second operand. The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Parameter to compare against. :returns: ``True``, if version is less than or equal the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`. """ return super().__le__(other) def __gt__(self, other: Union["CalendarVersion", str, int, None]) -> bool: """ Compare two version numbers if the version is greater than the second operand. The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Parameter to compare against. :returns: ``True``, if version is greater than the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`. """ return super().__gt__(other) def __ge__(self, other: Union["CalendarVersion", str, int, None]) -> bool: """ Compare two version numbers if the version is greater than or equal the second operand. The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major number is assumed (all other parts are zero). ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor number. :param other: Parameter to compare against. :returns: ``True``, if version is greater than or equal the second operand. :raises ValueError: If parameter ``other`` is None. :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`. """ return super().__ge__(other) def __hash__(self) -> int: return super().__hash__() def __format__(self, formatSpec: str) -> str: """ Return a string representation of this version number according to the format specification. .. topic:: Format Specifiers * ``%M`` - major number (year) * ``%m`` - minor number (month/week) :param formatSpec: The format specification. :return: Formatted version number. """ if formatSpec == "": return self.__str__() result = formatSpec # result = result.replace("%P", str(self._prefix)) result = result.replace("%M", str(self._major)) result = result.replace("%m", str(self._minor)) # result = result.replace("%p", str(self._pre)) return result.replace("%%", "%") def __repr__(self) -> str: """ Return a string representation of this version number without prefix ``v``. :returns: Raw version number representation without a prefix. """ return f"{self._major}.{self._minor}" def __str__(self) -> str: """ Return a string representation of this version number with prefix ``v``. :returns: Version number representation including a prefix. """ result = f"{self._major}" result += f".{self._minor}" if Parts.Minor in self._parts else "" return result @export class YearMonthVersion(CalendarVersion): """Representation of a calendar version number made of year and month like ``2021.10``.""" def __init__( self, year: int, month: Nullable[int] = None, build: Nullable[int] = None, flags: Flags = Flags.Clean, prefix: Nullable[str] = None, postfix: Nullable[str] = None ) -> None: """ Initializes a year-month version number representation. :param year: Year part of the version number. :param month: Month part of the version number. :param build: Build number part of the version number. :param flags: The version number's flags. :param prefix: The version number's prefix. :param postfix: The version number's postfix. :raises TypeError: If parameter 'major' is not of type int. :raises ValueError: If parameter 'major' is a negative number. :raises TypeError: If parameter 'minor' is not of type int. :raises ValueError: If parameter 'minor' is a negative number. :raises TypeError: If parameter 'micro' is not of type int. :raises ValueError: If parameter 'micro' is a negative number. :raises TypeError: If parameter 'build' is not of type int. :raises ValueError: If parameter 'build' is a negative number. :raises TypeError: If parameter 'prefix' is not of type str. :raises TypeError: If parameter 'postfix' is not of type str. """ super().__init__(year, month, 0, build, flags, prefix, postfix) @property def Month(self) -> int: """ Read-only property to access the month part. :return: The month part. """ return self._minor def __hash__(self) -> int: return super().__hash__() @export class YearWeekVersion(CalendarVersion): """Representation of a calendar version number made of year and week like ``2021.47``.""" def __init__( self, year: int, week: Nullable[int] = None, build: Nullable[int] = None, flags: Flags = Flags.Clean, prefix: Nullable[str] = None, postfix: Nullable[str] = None ) -> None: """ Initializes a year-week version number representation. :param year: Year part of the version number. :param week: Week part of the version number. :param build: Build number part of the version number. :param flags: The version number's flags. :param prefix: The version number's prefix. :param postfix: The version number's postfix. :raises TypeError: If parameter 'major' is not of type int. :raises ValueError: If parameter 'major' is a negative number. :raises TypeError: If parameter 'minor' is not of type int. :raises ValueError: If parameter 'minor' is a negative number. :raises TypeError: If parameter 'micro' is not of type int. :raises ValueError: If parameter 'micro' is a negative number. :raises TypeError: If parameter 'build' is not of type int. :raises ValueError: If parameter 'build' is a negative number. :raises TypeError: If parameter 'prefix' is not of type str. :raises TypeError: If parameter 'postfix' is not of type str. """ super().__init__(year, week, 0, build, flags, prefix, postfix) @property def Week(self) -> int: """ Read-only property to access the week part. :return: The week part. """ return self._minor def __hash__(self) -> int: return super().__hash__() @export class YearReleaseVersion(CalendarVersion): """Representation of a calendar version number made of year and release per year like ``2021.2``.""" def __init__( self, year: int, release: Nullable[int] = None, build: Nullable[int] = None, flags: Flags = Flags.Clean, prefix: Nullable[str] = None, postfix: Nullable[str] = None ) -> None: """ Initializes a year-release version number representation. :param year: Year part of the version number. :param release: Release number of the version number. :param build: Build number part of the version number. :param flags: The version number's flags. :param prefix: The version number's prefix. :param postfix: The version number's postfix. :raises TypeError: If parameter 'major' is not of type int. :raises ValueError: If parameter 'major' is a negative number. :raises TypeError: If parameter 'minor' is not of type int. :raises ValueError: If parameter 'minor' is a negative number. :raises TypeError: If parameter 'micro' is not of type int. :raises ValueError: If parameter 'micro' is a negative number. :raises TypeError: If parameter 'build' is not of type int. :raises ValueError: If parameter 'build' is a negative number. :raises TypeError: If parameter 'prefix' is not of type str. :raises TypeError: If parameter 'postfix' is not of type str. """ super().__init__(year, release, 0, build, flags, prefix, postfix) @property def Release(self) -> int: """ Read-only property to access the release number. :return: The release number. """ return self._minor def __hash__(self) -> int: return super().__hash__() @export class YearMonthDayVersion(CalendarVersion): """Representation of a calendar version number made of year, month and day like ``2021.10.15``.""" def __init__( self, year: int, month: Nullable[int] = None, day: Nullable[int] = None, build: Nullable[int] = None, flags: Flags = Flags.Clean, prefix: Nullable[str] = None, postfix: Nullable[str] = None ) -> None: """ Initializes a year-month-day version number representation. :param year: Year part of the version number. :param month: Month part of the version number. :param day: Day part of the version number. :param build: Build number part of the version number. :param flags: The version number's flags. :param prefix: The version number's prefix. :param postfix: The version number's postfix. :raises TypeError: If parameter 'major' is not of type int. :raises ValueError: If parameter 'major' is a negative number. :raises TypeError: If parameter 'minor' is not of type int. :raises ValueError: If parameter 'minor' is a negative number. :raises TypeError: If parameter 'micro' is not of type int. :raises ValueError: If parameter 'micro' is a negative number. :raises TypeError: If parameter 'build' is not of type int. :raises ValueError: If parameter 'build' is a negative number. :raises TypeError: If parameter 'prefix' is not of type str. :raises TypeError: If parameter 'postfix' is not of type str. """ super().__init__(year, month, day, build, flags, prefix, postfix) @property def Month(self) -> int: """ Read-only property to access the month part. :return: The month part. """ return self._minor @property def Day(self) -> int: """ Read-only property to access the day part. :return: The day part. """ return self._micro def __hash__(self) -> int: return super().__hash__() V = TypeVar("V", bound=Version) @export class RangeBoundHandling(Flag): """ A flag defining how to handle bounds in a range. If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds. """ BothBoundsInclusive = 0 #: Lower and upper bound are inclusive. LowerBoundInclusive = 0 #: Lower bound is inclusive. UpperBoundInclusive = 0 #: Upper bound is inclusive. LowerBoundExclusive = 1 #: Lower bound is exclusive. UpperBoundExclusive = 2 #: Upper bound is exclusive. BothBoundsExclusive = 3 #: Lower and upper bound are exclusive. @export class VersionRange(Generic[V], metaclass=ExtendedType, slots=True): """ Representation of a version range described by a lower bound and upper bound version. This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes. """ _lowerBound: V _upperBound: V _boundHandling: RangeBoundHandling def __init__(self, lowerBound: V, upperBound: V, boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive) -> None: """ Initializes a version range described by a lower and upper bound. :param lowerBound: lowest version (inclusive). :param upperBound: hightest version (inclusive). :raises TypeError: If parameter ``lowerBound`` is not of type :class:`Version`. :raises TypeError: If parameter ``upperBound`` is not of type :class:`Version`. :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types. :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``. """ if not isinstance(lowerBound, Version): ex = TypeError(f"Parameter 'lowerBound' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.") raise ex if not isinstance(upperBound, Version): ex = TypeError(f"Parameter 'upperBound' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.") raise ex if not ((lBC := lowerBound.__class__) is (uBC := upperBound.__class__) or issubclass(lBC, uBC) or issubclass(uBC, lBC)): ex = TypeError(f"Parameters 'lowerBound' and 'upperBound' are not compatible with each other.") ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and type '{getFullyQualifiedName(upperBound)}' for upperBound.") raise ex if not (lowerBound <= upperBound): ex = ValueError(f"Parameter 'lowerBound' isn't less than parameter 'upperBound'.") ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.") raise ex self._lowerBound = lowerBound self._upperBound = upperBound self._boundHandling = boundHandling @property def LowerBound(self) -> V: """ Property to access the range's lower bound. :return: Lower bound of the version range. """ return self._lowerBound @LowerBound.setter def LowerBound(self, value: V) -> None: if not isinstance(value, Version): ex = TypeError(f"Parameter 'value' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex self._lowerBound = value @readonly def UpperBound(self) -> V: """ Property to access the range's upper bound. :return: Upper bound of the version range. """ return self._upperBound @UpperBound.setter def UpperBound(self, value: V) -> None: if not isinstance(value, Version): ex = TypeError(f"Parameter 'value' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex self._upperBound = value @readonly def BoundHandling(self) -> RangeBoundHandling: """ Property to access the range's bound handling strategy. :return: The range's bound handling strategy. """ return self._boundHandling @BoundHandling.setter def BoundHandling(self, value: RangeBoundHandling) -> None: if not isinstance(value, RangeBoundHandling): ex = TypeError(f"Parameter 'value' is not of type 'RangeBoundHandling'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex self._boundHandling = value def __and__(self, other: Any) -> "VersionRange[T]": """ Compute the intersection of two version ranges. :param other: Second version range to intersect with. :returns: Intersected version range. :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`. :raises ValueError: If intersection is empty. """ if not isinstance(other, VersionRange): ex = TypeError(f"Parameter 'other' is not of type 'VersionRange'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex if not (isinstance(other._lowerBound, self._lowerBound.__class__) and isinstance(self._lowerBound, other._lowerBound.__class__)): ex = TypeError(f"Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible with each other.") ex.add_note( f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.") raise ex if other._lowerBound < self._lowerBound: lBound = self._lowerBound elif other._lowerBound in self: lBound = other._lowerBound else: raise ValueError() if other._upperBound > self._upperBound: uBound = self._upperBound elif other._upperBound in self: uBound = other._upperBound else: raise ValueError() return self.__class__(lBound, uBound) def __lt__(self, other: Any) -> bool: """ Compare a version range and a version numbers if the version range is less than the second operand (version). :param other: Operand to compare against. :returns: ``True``, if version range is less than the second operand (version). :raises TypeError: If parameter ``other`` is not of type :class:`Version`. """ # TODO: support VersionRange < VersionRange too # TODO: support str, int, ... like Version ? if not isinstance(other, Version): ex = TypeError(f"Parameter 'other' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): ex = TypeError(f"Parameter 'other' is not compatible with version range.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._upperBound < other def __le__(self, other: Any) -> bool: """ Compare a version range and a version numbers if the version range is less than or equal the second operand (version). :param other: Operand to compare against. :returns: ``True``, if version range is less than or equal the second operand (version). :raises TypeError: If parameter ``other`` is not of type :class:`Version`. """ # TODO: support VersionRange < VersionRange too # TODO: support str, int, ... like Version ? if not isinstance(other, Version): ex = TypeError(f"Parameter 'other' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): ex = TypeError(f"Parameter 'other' is not compatible with version range.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex if RangeBoundHandling.UpperBoundExclusive in self._boundHandling: return self._upperBound < other else: return self._upperBound <= other def __gt__(self, other: Any) -> bool: """ Compare a version range and a version numbers if the version range is greater than the second operand (version). :param other: Operand to compare against. :returns: ``True``, if version range is greater than the second operand (version). :raises TypeError: If parameter ``other`` is not of type :class:`Version`. """ # TODO: support VersionRange < VersionRange too # TODO: support str, int, ... like Version ? if not isinstance(other, Version): ex = TypeError(f"Parameter 'other' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): ex = TypeError(f"Parameter 'other' is not compatible with version range.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._lowerBound > other def __ge__(self, other: Any) -> bool: """ Compare a version range and a version numbers if the version range is greater than or equal the second operand (version). :param other: Operand to compare against. :returns: ``True``, if version range is greater than or equal the second operand (version). :raises TypeError: If parameter ``other`` is not of type :class:`Version`. """ # TODO: support VersionRange < VersionRange too # TODO: support str, int, ... like Version ? if not isinstance(other, Version): ex = TypeError(f"Parameter 'other' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): ex = TypeError(f"Parameter 'other' is not compatible with version range.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: return self._lowerBound > other else: return self._lowerBound >= other def __contains__(self, version: Version) -> bool: """ Check if the version is in the version range. :param version: Version to check. :returns: ``True``, if version is in range. :raises TypeError: If parameter ``version`` is not of type :class:`Version`. """ if not isinstance(version, Version): ex = TypeError(f"Parameter 'item' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") raise ex if self._boundHandling is RangeBoundHandling.BothBoundsInclusive: return self._lowerBound <= version <= self._upperBound elif self._boundHandling is (RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive): return self._lowerBound <= version < self._upperBound elif self._boundHandling is (RangeBoundHandling.LowerBoundExclusive | RangeBoundHandling.UpperBoundInclusive): return self._lowerBound < version <= self._upperBound else: return self._lowerBound < version < self._upperBound @export class VersionSet(Generic[V], metaclass=ExtendedType, slots=True): """ Representation of an ordered set of versions. This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes. """ _items: List[V] #: An ordered list of set members. def __init__(self, versions: Union[Version, Iterable[V]]) -> None: """ Initializes a version set either by a single version or an iterable of versions. :param versions: A single version or an iterable of versions. :raises ValueError: If parameter ``versions`` is None`. :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`. :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`. :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof. """ if versions is None: raise ValueError(f"Parameter 'versions' is None.") if isinstance(versions, Version): self._items = [versions] elif isinstance(versions, abc_Iterable): iterator = iter(versions) try: firstVersion = next(iterator) except StopIteration: self._items = [] return if not isinstance(firstVersion, Version): raise TypeError(f"First element in parameter 'versions' is not of type Version.") baseType = firstVersion.__class__ for version in iterator: if not isinstance(version, baseType): raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}") self._items = list(sorted(versions)) else: raise TypeError(f"Parameter 'versions' is not an Iterable.") def __and__(self, other: "VersionSet[V]") -> "VersionSet[T]": """ Compute intersection of two version sets. :param other: Second set of versions. :returns: Intersection of two version sets. """ selfIterator = self.__iter__() otherIterator = other.__iter__() result = [] try: selfValue = next(selfIterator) otherValue = next(otherIterator) while True: if selfValue < otherValue: selfValue = next(selfIterator) elif otherValue < selfValue: otherValue = next(otherIterator) else: result.append(selfValue) selfValue = next(selfIterator) otherValue = next(otherIterator) except StopIteration: pass return VersionSet(result) def __or__(self, other: "VersionSet[V]") -> "VersionSet[T]": """ Compute union of two version sets. :param other: Second set of versions. :returns: Union of two version sets. """ selfIterator = self.__iter__() otherIterator = other.__iter__() result = [] try: selfValue = next(selfIterator) except StopIteration: for otherValue in otherIterator: result.append(otherValue) try: otherValue = next(otherIterator) except StopIteration: for selfValue in selfIterator: result.append(selfValue) while True: if selfValue < otherValue: result.append(selfValue) try: selfValue = next(selfIterator) except StopIteration: result.append(otherValue) for otherValue in otherIterator: result.append(otherValue) break elif otherValue < selfValue: result.append(otherValue) try: otherValue = next(otherIterator) except StopIteration: result.append(selfValue) for selfValue in selfIterator: result.append(selfValue) break else: result.append(selfValue) try: selfValue = next(selfIterator) except StopIteration: for otherValue in otherIterator: result.append(otherValue) break try: otherValue = next(otherIterator) except StopIteration: for selfValue in selfIterator: result.append(selfValue) break return VersionSet(result) def __lt__(self, other: Any) -> bool: """ Compare a version set and a version numbers if the version set is less than the second operand (version). :param other: Operand to compare against. :returns: ``True``, if version set is less than the second operand (version). :raises TypeError: If parameter ``other`` is not of type :class:`Version`. """ # TODO: support VersionRange < VersionRange too # TODO: support str, int, ... like Version ? if not isinstance(other, Version): ex = TypeError(f"Parameter 'other' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._items[-1] < other def __le__(self, other: Any) -> bool: """ Compare a version set and a version numbers if the version set is less than or equal the second operand (version). :param other: Operand to compare against. :returns: ``True``, if version set is less than or equal the second operand (version). :raises TypeError: If parameter ``other`` is not of type :class:`Version`. """ # TODO: support VersionRange < VersionRange too # TODO: support str, int, ... like Version ? if not isinstance(other, Version): ex = TypeError(f"Parameter 'other' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._items[-1] <= other def __gt__(self, other: Any) -> bool: """ Compare a version set and a version numbers if the version set is greater than the second operand (version). :param other: Operand to compare against. :returns: ``True``, if version set is greater than the second operand (version). :raises TypeError: If parameter ``other`` is not of type :class:`Version`. """ # TODO: support VersionRange < VersionRange too # TODO: support str, int, ... like Version ? if not isinstance(other, Version): ex = TypeError(f"Parameter 'other' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._items[0] > other def __ge__(self, other: Any) -> bool: """ Compare a version set and a version numbers if the version set is greater than or equal the second operand (version). :param other: Operand to compare against. :returns: ``True``, if version set is greater than or equal the second operand (version). :raises TypeError: If parameter ``other`` is not of type :class:`Version`. """ # TODO: support VersionRange < VersionRange too # TODO: support str, int, ... like Version ? if not isinstance(other, Version): ex = TypeError(f"Parameter 'other' is not of type 'Version'.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") raise ex return self._items[0] >= other def __contains__(self, version: V) -> bool: """ Checks if the version a member of the set. :param version: The version to check. :returns: ``True``, if the version is a member of the set. """ return version in self._items def __len__(self) -> int: """ Returns the number of members in the set. :returns: Number of set members. """ return len(self._items) def __iter__(self) -> Iterator[V]: """ Returns an iterator to iterate all versions of this set from lowest to highest. :returns: Iterator to iterate versions. """ return self._items.__iter__() def __getitem__(self, index: int) -> V: """ Access to a version of a set by index. :param index: The index of the version to access. :returns: The indexed version. .. hint:: Versions are ordered from lowest to highest version number. """ return self._items[index] pyTooling-8.11.0/pyTooling/Warning/000077500000000000000000000000001513317154500171635ustar00rootroot00000000000000pyTooling-8.11.0/pyTooling/Warning/__init__.py000066400000000000000000000251701513317154500213010ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /_ _ _ __ _ __ (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ /\ / / _` | '__| '_ \| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V V / (_| | | | | | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/\_/ \__,_|_| |_| |_|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ A solution to send warnings like exceptions to a handler in the upper part of the call-stack. .. hint:: See :ref:`high-level help ` for explanations and usage examples. """ from threading import local from types import TracebackType from typing import List, Callable, Optional as Nullable, Type, Iterator, Self try: from pyTooling.Decorators import export, readonly from pyTooling.Common import getFullyQualifiedName from pyTooling.Exceptions import ExceptionBase except ModuleNotFoundError: # pragma: no cover print("[pyTooling.Warning] Could not import from 'pyTooling.*'!") try: from Decorators import export, readonly from Common import getFullyQualifiedName from Exceptions import ExceptionBase except ModuleNotFoundError as ex: # pragma: no cover print("[pyTooling.Warning] Could not import directly!") raise ex __all__ = ["_threadLocalData"] _threadLocalData = local() """A reference to the thread local data needed by the pyTooling.Warning classes.""" @export class Warning(BaseException): """ Base-exception of all warnings handled by :class:`WarningCollector`. .. tip:: Warnings can be unhandled within a call hierarchy. """ @export class CriticalWarning(BaseException): """ Base-exception of all critical warnings handled by :class:`WarningCollector`. .. tip:: Critical warnings must be unhandled within a call hierarchy, otherwise a :exc:`UnhandledCriticalWarningException` will be raised. """ @export class UnhandledWarningException(ExceptionBase): # FIXME: to be removed in v9.0.0 """ Deprecated. .. deprecated:: v9.0.0 Please use :exc:`UnhandledCriticalWarningException`. """ @export class UnhandledCriticalWarningException(UnhandledWarningException): """ This exception is raised when a critical warning isn't handled by a :class:`WarningCollector` within the call-hierarchy. """ @export class UnhandledExceptionException(UnhandledWarningException): """ This exception is raised when an exception isn't handled by a :class:`WarningCollector` within the call-hierarchy. """ @export class WarningCollector: """ A context manager to collect warnings within the call hierarchy. """ _parent: Nullable["WarningCollector"] #: Parent WarningCollector _warnings: List[BaseException] #: List of collected warnings (and exceptions). _handler: Nullable[Callable[[BaseException], bool]] #: Optional handler function, which is called per collected warning. def __init__( self, warnings: Nullable[List[BaseException]] = None, handler: Nullable[Callable[[BaseException], bool]] = None ) -> None: """ Initializes a warning collector. :param warnings: An optional reference to a list of warnings, which can be modified (appended) by this warning collector. If ``None``, an internal list is created and can be referenced by the collector's instance. :param handler: An optional handler function, which processes the current warning and decides if a warning should be reraised as an exception. :raises TypeError: If optional parameter 'warnings' is not of type list. :raises TypeError: If optional parameter 'handler' is not a callable. """ if warnings is None: warnings = [] elif not isinstance(warnings, list): ex = TypeError(f"Parameter 'warnings' is not list.") ex.add_note(f"Got type '{getFullyQualifiedName(warnings)}'.") raise ex if handler is not None and not isinstance(handler, Callable): ex = TypeError(f"Parameter 'handler' is not callable.") ex.add_note(f"Got type '{getFullyQualifiedName(handler)}'.") raise ex self._parent = None self._warnings = warnings self._handler = handler def __len__(self) -> int: """ Returns the number of collected warnings. :returns: Number of collected warnings. """ return len(self._warnings) def __iter__(self) -> Iterator[BaseException]: return iter(self._warnings) def __getitem__(self, index: int) -> BaseException: return self._warnings[index] def __enter__(self) -> Self: """ Enter the warning collector context. :returns: The warning collector instance. """ global _threadLocalData try: self.Parent = _threadLocalData.warningCollector except AttributeError: pass _threadLocalData.warningCollector = self return self def __exit__( self, exc_type: Nullable[Type[BaseException]] = None, exc_val: Nullable[BaseException] = None, exc_tb: Nullable[TracebackType] = None ) -> Nullable[bool]: """ Exit the warning collector context. :param exc_type: Exception type :param exc_val: Exception instance :param exc_tb: Exception's traceback. :returns: ``None`` """ global _threadLocalData _threadLocalData.warningCollector = self._parent @property def Parent(self) -> Nullable["WarningCollector"]: """ Property to access the parent warning collected. :returns: The parent warning collector or ``None``. """ return self._parent @Parent.setter def Parent(self, value: "WarningCollector") -> None: self._parent = value @readonly def Warnings(self) -> List[BaseException]: """ Read-only property to access the list of collected warnings. :returns: A list of collected warnings. """ return self._warnings def AddWarning(self, warning: BaseException) -> bool: """ Add a warning to the list of warnings managed by this warning collector. :param warning: The warning to add to the collectors internal warning list. :returns: Return ``True`` if the warning collector has a local handler callback and this handler returned ``True``; otherwise ``False``. :raises ValueError: If parameter ``warning`` is None. :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`. """ if warning is None: raise ValueError("Parameter 'warning' is None.") elif not isinstance(warning, (Warning, CriticalWarning, Exception)): ex = TypeError(f"Parameter 'warning' is not of type 'Warning', 'CriticalWarning' or 'Exception'.") ex.add_note(f"Got type '{getFullyQualifiedName(warning)}'.") raise ex self._warnings.append(warning) return False if self._handler is None else self._handler(warning) @classmethod def Raise(cls, warning: BaseException) -> None: """ Walk the callstack frame by frame upwards and search for the first warning collector. :param warning: Warning to send upwards in the call stack. :raises Exception: If warning should be converted to an exception. :raises Exception: If the call-stack walk couldn't find a warning collector. """ global _threadLocalData try: warningCollector = _threadLocalData.warningCollector if warningCollector.AddWarning(warning): raise Exception(f"Warning: {warning}") from warning except AttributeError: ex = None if isinstance(warning, Exception): ex = UnhandledExceptionException(f"Unhandled Exception: {warning}") elif isinstance(warning, CriticalWarning): ex = UnhandledCriticalWarningException(f"Unhandled Critical Warning: {warning}") if ex is not None: ex.add_note(f"Add a 'with'-statement using '{cls.__name__}' somewhere up the call-hierarchy to receive and collect warnings.") raise ex from warning pyTooling-8.11.0/pyTooling/py.typed000066400000000000000000000000001513317154500172430ustar00rootroot00000000000000pyTooling-8.11.0/pyproject.toml000066400000000000000000000031671513317154500165150ustar00rootroot00000000000000[build-system] requires = [ "setuptools >= 80.0", "wheel ~= 0.45.0", "pyTooling ~= 8.10" ] build-backend = "setuptools.build_meta" [tool.black] line-length = 120 [tool.mypy] packages = ["pyTooling"] python_version = "3.14" strict = true pretty = true show_error_context = true show_error_codes = true namespace_packages = true html_report = "report/typing/html" junit_xml = "report/typing/StaticTypingSummary.xml" cobertura_xml_report = "report/typing" [tool.pytest] addopts = ["--tb=native"] # Don't set 'python_classes = *' otherwise, pytest doesn't search for classes # derived from unittest.Testcase python_files = ["*"] python_functions = ["test_*"] filterwarnings = [ "error::DeprecationWarning", "error::PendingDeprecationWarning" ] junit_xml = "report/unit/UnittestReportSummary.xml" junit_logging = "all" [tool.pyedaa-reports] junit_xml = "report/unit/unittest.xml" [tool.interrogate] color = true verbose = 1 # possible values: 0 (minimal output), 1 (-v), 2 (-vv) fail-under = 59 exclude = [ "build", "dist", "doc", "tests", "setup.py" ] ignore-setters = true [tool.coverage.run] branch = true relative_files = true omit = [ "*site-packages*", "setup.py", "tests/benchmark/*", "tests/performance/*", "tests/platform/*", "tests/unit/*" ] [tool.coverage.report] skip_covered = false skip_empty = true exclude_lines = [ "pragma: no cover", "raise NotImplementedError" ] omit = [ "tests/*" ] [tool.coverage.xml] output = "report/coverage/coverage.xml" [tool.coverage.json] output = "report/coverage/coverage.json" [tool.coverage.html] directory = "report/coverage/html" title="Code Coverage of pyTooling" pyTooling-8.11.0/requirements.txt000066400000000000000000000000001513317154500170440ustar00rootroot00000000000000pyTooling-8.11.0/run.ps1000066400000000000000000000271661513317154500150370ustar00rootroot00000000000000[CmdletBinding()] Param( # Clean up all files and directories [switch]$clean, # Commands [switch]$all, [switch]$copyall, [switch]$doc, [switch]$livedoc, [switch]$doccov, [switch]$docview, [switch]$unit, [switch]$liveunit, [switch]$copyunit, [switch]$cov, [switch]$livecov, [switch]$copycov, [switch]$type, [switch]$livetype, [switch]$copytype, [switch]$nooutput, [switch]$build, [switch]$install, # Display this help" [switch]$help ) $PackageName = "pyTooling" $PackageVersion = "8.11.0" # set default values $EnableDebug = [bool]$PSCmdlet.MyInvocation.BoundParameters["Debug"] $EnableVerbose = [bool]$PSCmdlet.MyInvocation.BoundParameters["Verbose"] -or $EnableDebug # Display help if no command was selected $help = $help -or ( -not( $all -or $copyall -or $clean -or $doc -or $livedoc -or $doccov -or $docview -or $unit -or $liveunit -or $copyunit -or $cov -or $livecov -or $copycov -or $type -or $livetype -or $copytype -or $build -or $install ) ) Write-Host "================================================================================" -ForegroundColor Magenta Write-Host "$PackageName Documentation Compilation and Assembly Tool" -ForegroundColor Magenta Write-Host "================================================================================" -ForegroundColor Magenta if ($help) { Get-Help $MYINVOCATION.MyCommand.Path -Detailed exit 0 } if ($all) { $doc = $true $unit = $true # $copyunit = $true $cov = $true # $copycov = $true $type = $true $copytype = $true } if ($copyall) {# $copyunit = $true # $copycov = $true $copytype = $true } if ($clean) { Write-Host -ForegroundColor DarkYellow "[live][DOC] Cleaning documentation directories ..." rm -Force .\doc\$PackageName\* .\doc\make.bat clean Write-Host -ForegroundColor DarkYellow "[live][BUILD] Cleaning build directories ..." rm -Force .\build\bdist.win-amd64 rm -Force .\build\lib } if ($build) { Write-Host -ForegroundColor Yellow "[live][BUILD] Cleaning build directories ..." rm -Force .\build\bdist.win-amd64 rm -Force .\build\lib Write-Host -ForegroundColor Yellow "[live][BUILD] Building $PackageName package as wheel ..." py -3.14 -m build --wheel --no-isolation Write-Host -ForegroundColor Yellow "[live][BUILD] Building wheel finished" } if ($install) { if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Write-Host -ForegroundColor Yellow "[live][INSTALL] Installing $PackageName with administrator rights ..." $proc = Start-Process pwsh.exe "-NoProfile -ExecutionPolicy Bypass -WorkingDirectory `"$PSScriptRoot`" -File `"$PSCommandPath`" `"-install`"" -Verb RunAs -Wait # Write-Host -ForegroundColor Yellow "[live][INSTALL] Wait on administrator console ..." # Wait-Process -Id $proc.Id } else { Write-Host -ForegroundColor Cyan "[ADMIN][UNINSTALL] Uninstalling $PackageName ..." py -3.14 -m pip uninstall -y $PackageName Write-Host -ForegroundColor Cyan "[ADMIN][INSTALL] Installing $PackageName from wheel ..." py -3.14 -m pip install .\dist\$PackageName-$PackageVersion-py3-none-any.whl Write-Host -ForegroundColor Cyan "[ADMIN][INSTALL] Closing window in 5 seconds ..." Start-Sleep -Seconds 5 } } $jobs = @() if ($livedoc) { Write-Host -ForegroundColor DarkYellow "[live][DOC] Building documentation using Sphinx ..." cd doc py -3.14 -m sphinx.cmd.build -b html . _build/html --doctree-dir _build/doctrees --jobs auto --warning-file _build/sphinx-warnings.log --verbose cd .. Write-Host -ForegroundColor DarkYellow "[live][DOC] Documentation finished" } elseif ($doc) { Write-Host -ForegroundColor DarkYellow "[Job1][DOC] Building documentation using Sphinx ..." Write-Host -ForegroundColor DarkGreen "[SCRIPT] Starting Documentation job ..." # Compile documentation $compileDocFunc = { cd doc py -3.14 -m sphinx.cmd.build -b html . _build/html --doctree-dir _build/doctrees --jobs auto --warning-file _build/sphinx-warnings.log --verbose } $docJob = Start-Job -Name "Documentation" -ScriptBlock $compileDocFunc # $jobs += $docJob } if ($doccov) { .\doc\make.bat coverage } if ($liveunit) { Write-Host -ForegroundColor DarkYellow "[live][UNIT] Running Unit Tests using pytest ..." $env:ENVIRONMENT_NAME = "Windows (x86-64)" pytest -raP --color=yes --junitxml=report/unit/TestReportSummary.xml --template=html1/index.html --report=report/unit/html/index.html --split-report tests/unit pyedaa-reports -v unittest "--merge=pyTest-JUnit:report/unit/TestReportSummary.xml" "--name=$PackageName" "--pytest=rewrite-dunder-init;reduce-depth:pytest.tests.unit" "--output=pyTest-JUnit:report/unit/unittest.xml" if ($copyunit) { cp -Recurse -Force .\report\unit\html\* .\doc\_build\html\unittests Write-Host -ForegroundColor DarkBlue "[live][UNIT] Copyed unit testing report to 'unittests' directory in HTML directory" } Write-Host -ForegroundColor DarkYellow "[live][UNIT] Unit Tests finished" } elseif ($unit) { Write-Host -ForegroundColor DarkYellow "[Job2][UNIT] Running Unit Tests using pytest ..." Write-Host -ForegroundColor DarkGreen "[SCRIPT] Starting UnitTests jobs ..." # Run unit tests $runUnitFunc = { $env:ENVIRONMENT_NAME = "Windows (x86-64)" pytest -raP --color=yes --junitxml=report/unit/TestReportSummary.xml --template=html1/index.html --report=report/unit/html/index.html --split-report tests/unit pyedaa-reports -v unittest "--merge=pyTest-JUnit:report/unit/TestReportSummary.xml" "--name=$PackageName" "--pytest=rewrite-dunder-init;reduce-depth:pytest.tests.unit" "--output=pyTest-JUnit:report/unit/unittest.xml" } $unitJob = Start-Job -Name "UnitTests" -ScriptBlock $runUnitFunc $jobs += $unitJob } if ($livecov) { Write-Host -ForegroundColor DarkMagenta "[live][COV] Running Unit Tests with coverage ..." $env:ENVIRONMENT_NAME = "Windows (x86-64)" coverage run --data-file=.coverage --rcfile=pyproject.toml -m pytest -ra --tb=line --color=yes tests/unit Write-Host -ForegroundColor DarkMagenta "[live][COV] Convert coverage report to HTML ..." coverage html Write-Host -ForegroundColor DarkMagenta "[live][COV] Convert coverage report to XML (Cobertura) ..." coverage xml Write-Host -ForegroundColor DarkMagenta "[live][COV] Convert coverage report to JSON ..." coverage json Write-Host -ForegroundColor DarkMagenta "[live][COV] Write coverage report to console ..." coverage report if ($copycov) { cp -Recurse -Force .\report\coverage\html\* .\doc\_build\html\coverage Write-Host -ForegroundColor DarkMagenta "[live][COV] Copyed code coverage report to 'coverage' directory in HTML directory" } Write-Host -ForegroundColor DarkMagenta "[live][COV] Coverage finished" } elseif ($cov) { Write-Host -ForegroundColor DarkMagenta "[live][COV] Running Unit Tests with coverage ..." Write-Host -ForegroundColor DarkMagenta "[SCRIPT] Starting Coverage jobs ..." # Collect coverage $collectCovFunc = { $env:ENVIRONMENT_NAME = "Windows (x86-64)" coverage run --data-file=.coverage --rcfile=pyproject.toml -m pytest -ra --tb=line --color=yes tests/unit Write-Host -ForegroundColor DarkMagenta "[Job3][COV] Convert coverage report to HTML ..." coverage html Write-Host -ForegroundColor DarkMagenta "[Job3][COV] Convert coverage report to XML (Cobertura) ..." coverage xml Write-Host -ForegroundColor DarkMagenta "[Job3][COV] Convert coverage report to JSON ..." coverage json } $covJob = Start-Job -Name "Coverage" -ScriptBlock $collectCovFunc $jobs += $covJob } if ($livetype) { Write-Host -ForegroundColor DarkCyan "[live][TYPE] Running static type analysis using mypy ..." $env:MYPY_FORCE_COLOR = 1 mypy.exe -p $PackageName if ($copytype) { cp -Recurse -Force .\report\typing\* .\doc\_build\html\typing Write-Host -ForegroundColor DarkCyan "[live][TYPE] Copyed typing report to 'typing' directory in HTML directory." } Write-Host -ForegroundColor DarkCyan "[live][TYPE] Static type analysis finished" } elseif ($type) { Write-Host -ForegroundColor DarkCyan "[live][TYPE] Running static type analysis using mypy ..." Write-Host -ForegroundColor DarkCyan "[SCRIPT] Starting Typing jobs ..." # Analyze types $analyzeTypesFunc = { $env:MYPY_FORCE_COLOR = 1 mypy.exe -p $PackageName } $typeJob = Start-Job -Name "Typing" -ScriptBlock $analyzeTypesFunc $jobs += $typeJob } if ($doc) { Write-Host -ForegroundColor DarkGreen "[SCRIPT] Waiting on Documentation job ..." Wait-Job -Job $docJob Write-Host -ForegroundColor DarkYellow "[Job1][DOC] Documentation finished" } if ($jobs.Count -ne 0) { Write-Host -ForegroundColor DarkGreen ( "[SCRIPT] Waiting on {0} jobs ({1}) ..." -f $jobs.Count, (($jobs | %{ $_.Name }) -join ", ")) Wait-Job -Job $jobs } if (-not $liveunit -and $copyunit) { # if ($unit) # { Wait-Job -Job $unitJob # Write-Host -ForegroundColor DarkBlue "[Job2][UNIT] Unit tests finished" # } cp -Recurse -Force .\report\unit\html\* .\doc\_build\html\unittests Write-Host -ForegroundColor DarkBlue "[post][UNIT] Copyed unit testing report to 'unittests' directory in HTML directory" } if (-not ($livecov -or $cov) -and $copycov) { # if ($cov) # { Wait-Job -Job $unitJob # Write-Host -ForegroundColor DarkMagenta "[Job3][UNIT] Coverage collection finished" # } cp -Recurse -Force .\report\coverage\html\* .\doc\_build\html\coverage Write-Host -ForegroundColor DarkMagenta "[post][COV] Copyed code coverage report to 'coverage' directory in HTML directory" } if (-not $livetype -and $copytype) { # if ($type) # { Wait-Job -Job $typeJob # Write-Host -ForegroundColor DarkCyan "[Job4][UNIT] Static type analysis finished" # } cp -Recurse -Force .\report\typing\* .\doc\_build\html\typing Write-Host -ForegroundColor DarkCyan "[post][TYPE] Copyed typing report to 'typing' directory in HTML directory." } if ($type) { Write-Host -ForegroundColor DarkCyan "================================================================================" if (-not $nooutput) { Receive-Job -Job $typeJob } Remove-Job -Job $typeJob } if ($doc) { Write-Host -ForegroundColor DarkYellow "================================================================================" if (-not $nooutput) { Receive-Job -Job $docJob } Remove-Job -Job $docJob } if ($unit) { Write-Host -ForegroundColor DarkBlue "================================================================================" if (-not $nooutput) { Receive-Job -Job $unitJob } Remove-Job -Job $unitJob } if ($cov) { Write-Host -ForegroundColor DarkMagenta "================================================================================" if (-not $nooutput) { Receive-Job -Job $covJob } Remove-Job -Job $covJob if ($copycov) { cp -Recurse -Force .\report\coverage\html\* .\doc\_build\html\coverage Write-Host -ForegroundColor DarkMagenta "[post][COV] Copyed code coverage report to 'coverage' directory in HTML directory" } } if ($docview) { & 'C:\Program Files\Google\Chrome\Application\chrome.exe' "$(pwd)\doc\_build\html\index.html" } Write-Host -ForegroundColor DarkGreen "================================================================================" Write-Host -ForegroundColor DarkGreen "[SCRIPT] Finished" pyTooling-8.11.0/setup.py000066400000000000000000000114321513317154500153050ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, | # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Package installer for 'pyTooling is a powerful collection of arbitrary useful classes, decorators, meta-classes and exceptions.'. """ # Add package itself to PYTHON_PATH, so it can be used to package itself. from os.path import abspath from sys import path as sys_path sys_path.insert(0, abspath('./pyTooling')) from setuptools import setup from pathlib import Path from Packaging import DescribePythonPackageHostedOnGitHub gitHubNamespace = "pyTooling" packageName = "pyTooling.*" packageDirectory = packageName[:-2] packageInformationFile = Path(f"{packageDirectory}/Common/__init__.py") setup( **DescribePythonPackageHostedOnGitHub( packageName=packageName, description="pyTooling is a powerful collection of arbitrary useful classes, decorators, meta-classes and exceptions.", gitHubNamespace=gitHubNamespace, unittestRequirementsFile=Path("tests/requirements.txt"), additionalRequirements={ "pypi": ["aiohttp >= 3.12", "packaging >= 25.0", "requests >= 2.32"], # aiohttp limited on MSYS2 to 3.12.x "packaging": ["setuptools >= 80.0"], "terminal": ["colorama ~= 0.4.6"], "yaml": ["ruamel.yaml ~= 0.18"], }, sourceFileWithVersion=packageInformationFile, pythonVersions=("3.11", "3.12", "3.13", "3.14"), dataFiles={ packageName[:-1] + "Common": ["../py.typed"] }, debug=True ) ) pyTooling-8.11.0/tests/000077500000000000000000000000001513317154500147345ustar00rootroot00000000000000pyTooling-8.11.0/tests/README.md000066400000000000000000000001011513317154500162030ustar00rootroot00000000000000# Tests ## Benchmark tests ## Performance tests ## Unit tests pyTooling-8.11.0/tests/__init__.py000066400000000000000000000067271513317154500170610ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, | # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Test code for pyTooling.""" pyTooling-8.11.0/tests/benchmark/000077500000000000000000000000001513317154500166665ustar00rootroot00000000000000pyTooling-8.11.0/tests/benchmark/Common/000077500000000000000000000000001513317154500201165ustar00rootroot00000000000000pyTooling-8.11.0/tests/benchmark/Common/MergeDicts.py000066400000000000000000000133051513317154500225200ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Benchmark tests for :func:`pyTooling.Common.mergedicts`.""" from pytest import mark from pyTooling.Common import mergedicts if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) dictA_10 = {str(i): i for i in range(0, 10)} dictB_10 = {str(i): i for i in range(10, 10)} dictC_10 = {str(i): i for i in range(20, 10)} dictD_10 = {str(i): i for i in range(30, 10)} dictA_100 = {str(i): i for i in range(0, 100)} dictB_100 = {str(i): i for i in range(100, 100)} dictC_100 = {str(i): i for i in range(200, 100)} dictD_100 = {str(i): i for i in range(300, 100)} dictA_1000 = {str(i): i for i in range(0, 1000)} dictB_1000 = {str(i): i for i in range(1000, 1000)} dictC_1000 = {str(i): i for i in range(2000, 1000)} dictD_1000 = {str(i): i for i in range(3000, 1000)} @mark.benchmark(group="E0: Merge dictionaries with 10 items") def test_Merge1x10(benchmark) -> None: @benchmark def func(): z = mergedicts(dictA_10) @mark.benchmark(group="E0: Merge dictionaries with 10 items") def test_Merge2x10(benchmark) -> None: @benchmark def func(): z = mergedicts(dictA_10, dictB_10) @mark.benchmark(group="E0: Merge dictionaries with 10 items") def test_Merge3x10(benchmark) -> None: @benchmark def func(): z = mergedicts(dictA_10, dictB_10, dictC_10) @mark.benchmark(group="E0: Merge dictionaries with 10 items") def test_Merge4x10(benchmark) -> None: @benchmark def func(): z = mergedicts(dictA_10, dictB_10, dictC_10, dictD_10) @mark.benchmark(group="E1: Merge dictionaries with 100 items") def test_Merge2x100(benchmark) -> None: @benchmark def func(): z = mergedicts(dictA_100, dictB_100) @mark.benchmark(group="E1: Merge dictionaries with 100 items") def test_Merge4x100(benchmark) -> None: @benchmark def func(): z = mergedicts(dictA_100, dictB_100, dictC_100, dictD_100) @mark.benchmark(group="E2: Merge dictionaries with 1000 items") def test_Merge2x1000(benchmark) -> None: @benchmark def func(): z = mergedicts(dictA_1000, dictB_1000) @mark.benchmark(group="E2: Merge dictionaries with 1000 items") def test_Merge4x1000(benchmark) -> None: @benchmark def func(): z = mergedicts(dictA_1000, dictB_1000, dictC_1000, dictD_1000) pyTooling-8.11.0/tests/benchmark/Common/ZipDicts.py000066400000000000000000000132141513317154500222220ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Benchmark tests for :func:`pyTooling.Common.zipdicts`.""" from pytest import mark from pyTooling.Common import zipdicts if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) dictA_10 = {str(i): i for i in range(10)} dictB_10 = {str(i): i*10 for i in range(10)} dictC_10 = {str(i): i*20 for i in range(10)} dictD_10 = {str(i): i*30 for i in range(10)} dictA_100 = {str(i): i for i in range(100)} dictB_100 = {str(i): i*100 for i in range(100)} dictC_100 = {str(i): i*200 for i in range(100)} dictD_100 = {str(i): i*300 for i in range(100)} dictA_1000 = {str(i): i for i in range(1000)} dictB_1000 = {str(i): i*1000 for i in range(1000)} dictC_1000 = {str(i): i*2000 for i in range(1000)} dictD_1000 = {str(i): i*3000 for i in range(1000)} @mark.benchmark(group="F0: Zip dictionaries with 10 items") def test_Zip1x10(benchmark) -> None: @benchmark def func(): z = zipdicts(dictA_10) @mark.benchmark(group="F0: Zip dictionaries with 10 items") def test_Zip2x10(benchmark) -> None: @benchmark def func(): z = zipdicts(dictA_10, dictB_10) @mark.benchmark(group="F0: Zip dictionaries with 10 items") def test_Zip3x10(benchmark) -> None: @benchmark def func(): z = zipdicts(dictA_10, dictB_10, dictC_10) @mark.benchmark(group="F0: Zip dictionaries with 10 items") def test_Zip4x10(benchmark) -> None: @benchmark def func(): z = zipdicts(dictA_10, dictB_10, dictC_10, dictD_10) @mark.benchmark(group="F1: Zip dictionaries with 100 items") def test_Zip2x100(benchmark) -> None: @benchmark def func(): z = zipdicts(dictA_100, dictB_100) @mark.benchmark(group="F1: Zip dictionaries with 100 items") def test_Zip4x100(benchmark) -> None: @benchmark def func(): z = zipdicts(dictA_100, dictB_100, dictC_100, dictD_100) @mark.benchmark(group="F2: Zip dictionaries with 1000 items") def test_Zip2x1000(benchmark) -> None: @benchmark def func(): z = zipdicts(dictA_1000, dictB_1000) @mark.benchmark(group="F2: Zip dictionaries with 1000 items") def test_Zip4x1000(benchmark) -> None: @benchmark def func(): z = zipdicts(dictA_1000, dictB_1000, dictC_1000, dictD_1000) pyTooling-8.11.0/tests/benchmark/Common/__init__.py000066400000000000000000000067441513317154500222420ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Benchmark tests for pyTooling.Common.""" pyTooling-8.11.0/tests/benchmark/MetaClasses/000077500000000000000000000000001513317154500210725ustar00rootroot00000000000000pyTooling-8.11.0/tests/benchmark/MetaClasses/SlottedType.py000066400000000000000000000161371513317154500237340ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Benchmark tests for pyTooling.MetaClasses.ExtendedType.""" from pytest import mark from pyTooling.MetaClasses import ExtendedType if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class NormalNode_1: _data_0: int def __init__(self, data) -> None: self._data_0 = data def inc(self, add: int): self._data_0 = self._data_0 + add class SlottedNode_1(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data) -> None: self._data_0 = data def inc(self, add: int): self._data_0 = self._data_0 + add class NormalNode_10: _data_0: int _data_1: int _data_2: int _data_3: int _data_4: int _data_5: int _data_6: int _data_7: int _data_8: int _data_9: int def __init__(self, data) -> None: self._data_0 = data self._data_1 = data self._data_2 = data self._data_3 = data self._data_4 = data self._data_5 = data self._data_6 = data self._data_7 = data self._data_8 = data self._data_9 = data def inc(self, add: int): self._data_1 = self._data_0 + add self._data_2 = self._data_1 + add self._data_3 = self._data_2 + add self._data_4 = self._data_3 + add self._data_5 = self._data_4 + add self._data_6 = self._data_5 + add self._data_7 = self._data_6 + add self._data_8 = self._data_7 + add self._data_9 = self._data_8 + add class SlottedNode_10(metaclass=ExtendedType, slots=True): _data_0: int _data_1: int _data_2: int _data_3: int _data_4: int _data_5: int _data_6: int _data_7: int _data_8: int _data_9: int def __init__(self, data) -> None: self._data_0 = data self._data_1 = data self._data_2 = data self._data_3 = data self._data_4 = data self._data_5 = data self._data_6 = data self._data_7 = data self._data_8 = data self._data_9 = data def inc(self, add: int): self._data_1 = self._data_0 + add self._data_2 = self._data_1 + add self._data_3 = self._data_2 + add self._data_4 = self._data_3 + add self._data_5 = self._data_4 + add self._data_6 = self._data_5 + add self._data_7 = self._data_6 + add self._data_8 = self._data_7 + add self._data_9 = self._data_8 + add @mark.benchmark(group="B0: Create Objects with 1 slot") def test_CreateNormalObjects_1(benchmark) -> None: @benchmark def func(): [NormalNode_1(i) for i in range(1000)] @mark.benchmark(group="B0: Create Objects with 1 slot") def test_CreateSlottedObjects_1(benchmark) -> None: @benchmark def func(): [SlottedNode_1(i) for i in range(1000)] @mark.benchmark(group="B1: Create Objects with 10 slots") def test_CreateObjects_10(benchmark) -> None: @benchmark def func(): [NormalNode_10(i) for i in range(1000)] @mark.benchmark(group="B1: Create Objects with 10 slots") def test_CreateSlottedObjects_10(benchmark) -> None: @benchmark def func(): [SlottedNode_10(i) for i in range(1000)] @mark.benchmark(group="B2: Accumulate a single integer slot") def test_Accumulate_1(benchmark) -> None: @benchmark def func(): node = NormalNode_1(0) for i in range(1000): node.inc(i) @mark.benchmark(group="B2: Accumulate a single integer slot") def test_SlottedAccumulate_1(benchmark) -> None: @benchmark def func(): node = SlottedNode_1(0) for i in range(1000): node.inc(i) @mark.benchmark(group="B3: Accumulate 10 integer slots") def test_Accumulate_10(benchmark) -> None: @benchmark def func(): node = NormalNode_10(0) for i in range(1000): node.inc(i) @mark.benchmark(group="B3: Accumulate 10 integer slots") def test_SlottedAccumulate_10(benchmark) -> None: @benchmark def func(): node = SlottedNode_10(0) for i in range(1000): node.inc(i) pyTooling-8.11.0/tests/benchmark/MetaClasses/__init__.py000066400000000000000000000105111513317154500232010ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Benchmark tests for pyTooling.MetaClasses.""" from typing import Tuple, Any, Dict from pytest import mark if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class A: def __init__(self, arg) -> None: self.arg = arg class M(type): def __call__(cls, *args: Any, **kwargs: Any): newCls = cls.__new__(cls, *args, **kwargs) newCls.__init__(*args, **kwargs) return newCls class B(metaclass=M): def __init__(self, arg) -> None: self.arg = arg @mark.benchmark(group="A0: Create Objects") def test_CreateObjects_BuiltinCall(benchmark) -> None: @benchmark def func(): [A(i) for i in range(10)] @mark.benchmark(group="A0: Create Objects") def test_CreateObjects_UserDefinedCall(benchmark) -> None: @benchmark def func(): [B(i) for i in range(10)] pyTooling-8.11.0/tests/benchmark/__init__.py000066400000000000000000000067411513317154500210070ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _ \/ _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | __/ __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \___|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Benchmarks using pytest-benchmark.""" pyTooling-8.11.0/tests/benchmark/requirements.txt000066400000000000000000000002421513317154500221500ustar00rootroot00000000000000-r ../../requirements.txt # Coverage collection Coverage ~= 7.13 # Test Runner pytest ~= 9.0 pytest-cov ~= 7.0 # Test Runner extensions pytest-benchmark>=4.0.0 pyTooling-8.11.0/tests/data/000077500000000000000000000000001513317154500156455ustar00rootroot00000000000000pyTooling-8.11.0/tests/data/Graph/000077500000000000000000000000001513317154500167065ustar00rootroot00000000000000pyTooling-8.11.0/tests/data/Graph/EdgeLists/000077500000000000000000000000001513317154500205715ustar00rootroot00000000000000pyTooling-8.11.0/tests/data/Graph/EdgeLists/graph_n10000_m15000_dir_w0_100.edgelist000066400000000000000000005637331513317154500270610ustar00rootroot000000000000000 8120 22 0 6817 50 0 3901 15 1 5413 71 1 3531 63 3 1872 10 4 4191 63 5 7203 98 6 6347 79 6 7028 24 7 1821 52 8 4314 39 8 5095 5 8 6649 100 8 9152 49 9 2785 81 11 4352 68 11 5435 51 11 7066 94 13 6522 68 13 8594 37 14 6357 33 14 3268 0 15 5916 93 15 9864 99 16 3046 61 16 2821 29 16 1226 47 16 3582 43 17 5894 67 17 6647 82 17 402 57 18 9946 19 19 354 18 19 7368 19 20 608 6 23 5312 32 23 183 62 23 2123 94 23 9945 57 24 2558 66 25 5508 3 25 8421 49 27 9735 45 27 7924 64 27 3718 48 29 9916 85 29 1788 22 29 8341 72 30 4634 41 30 9208 6 30 3468 74 30 7692 66 30 4116 79 31 2494 85 31 7604 88 32 7 7 34 9157 86 35 4082 65 36 3068 95 37 1738 80 37 2976 70 37 6873 22 39 1656 11 39 5181 50 39 2276 94 39 4019 28 43 1063 52 45 4016 3 45 3084 74 46 3965 8 46 4113 23 48 5560 6 48 4023 35 48 2447 22 49 4827 17 49 8694 58 50 4793 13 50 7393 20 51 7605 27 51 2653 30 51 6970 26 51 9659 50 53 1141 9 53 5168 85 54 470 99 54 1438 13 54 9390 15 55 9365 96 55 4143 89 56 4995 86 57 7626 33 57 8128 53 57 5361 20 58 8530 32 58 6992 80 59 1574 33 59 107 62 60 245 27 60 8803 64 60 170 30 60 5297 47 62 6647 56 63 8625 69 63 8157 85 65 664 49 66 1510 60 68 213 25 68 3530 81 68 2840 85 71 4059 46 71 5857 75 71 8414 96 72 8983 97 72 1011 51 73 7699 25 76 8396 98 76 6226 68 76 6506 31 77 8711 76 78 2089 55 78 2905 33 79 8889 78 79 2966 50 79 169 23 79 3927 7 80 3460 51 82 2420 33 82 3808 40 82 9452 51 83 4634 87 83 781 19 84 587 51 84 836 42 85 6238 46 85 2343 82 85 8821 48 86 7840 52 88 7807 75 88 7650 78 89 1282 26 90 808 72 92 6865 20 92 6726 48 93 65 80 93 828 78 93 3250 90 95 1487 2 96 2802 85 98 4896 53 100 505 17 101 7437 78 101 7384 64 101 3853 54 103 1828 41 104 3875 25 106 4043 96 107 1831 2 108 3765 74 109 7766 58 111 1745 17 111 2628 30 111 1606 55 112 3558 84 112 7705 35 112 7162 99 112 8533 54 114 9662 95 115 847 31 116 5554 4 117 816 56 118 1317 91 119 6905 93 120 8839 20 120 6905 94 122 9458 14 122 8930 96 123 1399 9 123 7175 55 125 5004 77 126 13 62 128 1905 85 128 4782 44 129 713 29 130 8585 0 130 1661 71 130 4031 45 130 7382 33 130 8608 57 131 3801 20 132 1186 68 135 1041 98 135 5648 5 136 3463 44 136 8135 65 137 1107 19 138 1194 69 139 2941 91 140 6258 27 140 523 26 141 3581 19 142 9657 42 143 7925 22 144 8203 85 145 2982 36 145 5095 36 146 9543 79 146 9127 79 146 6486 33 147 8617 88 147 8968 12 148 1347 5 149 9492 37 149 7177 93 151 8293 61 151 5581 37 151 9544 26 152 9801 54 152 4464 36 152 887 69 153 8701 32 153 9290 59 153 4628 57 154 1597 37 155 9766 100 155 6657 53 157 5551 92 158 1295 78 160 4850 34 160 5521 51 161 803 3 161 1679 44 162 8270 84 162 4919 65 162 9422 98 163 3816 9 163 9830 27 165 4932 57 166 4326 22 167 2825 8 167 5545 40 168 2632 98 169 7156 17 169 1962 96 169 7057 57 169 1377 27 169 3610 49 170 1301 42 171 4813 22 171 5025 40 172 2255 99 173 7233 65 173 6373 27 173 4906 43 173 1081 21 174 2315 63 175 5400 52 176 5451 38 177 53 7 178 2303 31 178 3790 25 179 9968 81 179 9507 30 180 7621 26 180 4085 3 181 9014 34 181 105 52 182 8271 53 182 9178 83 184 3485 57 184 4830 17 185 3494 59 186 9488 28 187 3265 13 187 6351 99 187 4491 59 188 2969 56 189 9542 81 189 7605 91 190 1144 3 190 8478 6 190 9205 66 191 9469 64 191 865 63 192 5113 54 193 9376 23 193 9211 95 194 5120 99 194 5078 94 194 2286 97 195 5061 69 195 3123 14 196 3576 8 197 9106 78 197 1357 63 198 9756 28 198 8440 88 199 6586 89 199 4631 76 199 9148 87 199 5541 73 199 4689 34 199 7419 86 200 2507 98 205 3513 84 205 9166 42 206 4878 47 208 1012 46 208 1868 33 208 8974 19 208 7293 78 209 7315 40 210 7065 41 210 452 84 211 1422 74 212 9430 15 212 7305 97 212 4306 55 213 9201 11 214 3674 19 215 4274 75 215 3211 79 215 6310 81 216 9943 41 216 8743 55 217 2423 72 219 5405 82 220 1049 3 220 8506 20 220 6679 47 221 1936 68 222 523 61 223 7592 11 223 7539 97 224 9002 13 225 1305 25 225 9552 70 226 7792 96 227 9105 46 228 7045 49 229 3425 91 230 783 83 231 1001 39 233 9853 100 234 7698 33 234 4946 75 235 6134 21 236 6388 21 236 665 26 236 8289 18 237 3503 35 237 4762 70 237 338 29 238 8828 20 240 6914 14 240 7919 7 242 8337 89 242 7236 38 242 785 96 243 1550 26 246 9196 15 246 9814 60 247 5988 100 248 690 12 249 1841 32 250 3386 59 250 5092 75 251 1728 76 251 477 86 251 4735 86 252 6018 31 253 7776 32 253 8338 79 253 5394 67 255 2550 65 257 2920 97 257 6135 86 259 8992 84 259 9011 21 259 5839 99 261 1959 90 261 9273 68 263 7327 91 264 8877 69 264 1155 4 265 8439 50 265 6304 58 266 1337 0 266 3219 74 266 8344 19 267 1481 92 268 4947 60 269 6831 36 269 8911 64 269 5845 20 270 7319 89 270 3408 26 270 650 59 271 577 58 272 2372 30 272 5646 55 273 2624 26 273 4606 43 273 2019 10 274 4673 0 275 7489 59 277 5582 67 277 8490 17 277 9378 22 278 5916 14 280 7587 80 280 119 78 280 2878 45 281 3479 68 282 3760 2 283 1978 12 283 6307 80 284 2788 3 284 8595 32 284 1138 5 285 1128 70 285 9171 27 286 7466 87 286 115 59 287 6538 42 287 2372 9 288 4938 24 289 5122 28 289 6738 91 290 2011 97 291 5063 72 291 8495 7 292 4593 68 293 4185 68 293 9491 68 294 1765 87 294 9671 70 295 7927 87 295 9464 53 296 1138 56 296 6717 11 297 1974 80 297 4319 54 298 2738 55 300 4253 87 304 8257 96 304 9222 98 304 1542 20 305 2471 77 305 6468 22 306 7767 36 306 6521 92 309 2598 72 309 2318 53 310 942 21 310 5132 88 311 3731 47 311 758 2 311 3967 44 312 3929 63 314 6759 52 314 459 23 314 4483 24 315 1457 27 315 9006 16 316 4310 17 316 6302 12 316 1895 70 316 7006 100 317 6519 80 318 3434 33 320 1937 58 321 4190 52 321 3042 53 326 1783 87 327 4864 48 327 3848 49 328 7383 27 329 9832 60 329 5284 35 329 7420 97 330 1794 44 330 5920 34 330 5176 60 331 3027 23 332 3020 64 332 861 29 333 4175 7 333 6257 35 335 4094 83 335 1810 21 335 7249 61 337 4617 68 337 710 80 337 1051 53 337 9648 18 338 6245 67 339 6450 52 342 4500 20 342 3143 12 342 5964 16 345 8833 50 346 1145 28 347 2615 39 348 9871 82 349 4681 43 350 5410 100 351 7599 60 351 8926 93 352 930 69 352 1733 18 352 6528 35 353 3679 25 354 7692 73 355 4313 90 355 3201 21 355 5008 46 356 8713 25 356 1522 36 357 4252 56 357 4890 83 357 7582 28 358 9127 0 358 8316 10 359 8652 24 359 8296 67 359 1336 39 360 7221 53 361 7747 86 361 6225 90 361 8065 43 362 5516 58 362 1842 86 364 7077 27 364 4591 78 367 479 0 367 1278 91 368 1849 52 368 4935 95 368 1339 64 368 8054 41 368 5534 44 370 19 97 370 8509 11 371 849 22 372 7987 41 372 5069 2 372 2523 67 373 570 55 373 7319 93 374 6414 40 374 812 67 374 5290 44 374 1640 25 375 8028 45 376 8001 39 377 2089 47 377 9784 85 377 5006 58 378 725 66 379 2918 74 379 6334 80 380 4677 100 380 1245 26 381 2274 61 382 1859 18 383 4648 90 384 69 79 385 4053 58 385 9079 53 386 9970 1 386 5843 92 387 4150 47 389 861 40 390 9988 93 390 6370 36 391 1867 56 391 4806 61 392 6761 84 392 6216 85 392 4286 7 393 5886 55 395 1399 37 396 8537 62 397 2966 52 397 2419 70 399 760 67 399 4324 20 401 2829 8 401 2090 27 402 8775 19 402 4740 13 403 5155 2 403 7460 4 404 1947 32 404 4754 84 404 8034 11 406 6106 17 407 6154 38 407 8370 79 407 5529 40 408 7855 87 408 8388 57 408 9682 63 409 5374 14 409 3344 89 409 4877 28 409 6169 18 411 5230 60 413 6097 32 415 8345 63 415 1456 63 416 3315 32 416 5805 22 416 1228 40 416 9642 32 418 2867 26 421 4560 97 422 8571 11 422 9008 92 422 9210 2 423 7981 13 423 4802 22 425 5066 99 425 4466 62 427 6320 27 428 4675 45 428 3822 28 429 7440 17 430 3465 81 430 580 57 430 924 14 431 1026 50 431 4435 57 434 5074 23 435 3937 75 435 223 9 436 9485 76 436 9245 46 436 8504 38 437 1154 81 437 4949 84 438 4106 67 440 1372 4 440 9597 66 442 2279 21 443 2213 27 443 8813 35 444 1316 38 445 8083 67 445 8286 95 446 740 19 447 6558 14 447 7621 39 450 5317 59 450 7196 22 450 6234 32 451 1172 25 452 1847 6 452 499 27 452 1258 79 452 4587 38 453 2024 59 454 5861 95 455 1027 39 455 6412 45 457 1342 73 457 2416 10 457 6031 20 458 7558 39 458 3211 64 458 6101 16 459 8826 54 459 4000 60 460 4163 95 461 4154 3 461 1904 9 462 8605 36 464 9285 73 465 5386 34 465 8589 66 465 5707 21 467 2309 52 468 9400 55 468 8195 81 469 8861 77 469 7657 16 470 5239 80 470 1757 68 470 8944 100 470 3954 8 471 1827 6 472 6276 39 473 9694 90 477 8736 66 477 2308 80 478 9841 62 479 3860 63 479 3437 22 480 5390 73 480 6528 51 481 9784 82 481 3013 30 481 8779 84 481 1259 42 482 3419 55 482 604 21 483 299 62 484 9565 36 484 6840 23 485 9064 60 486 5855 62 487 1893 29 487 8397 65 487 4573 61 488 7255 30 488 2438 60 489 8048 22 490 7178 98 490 7393 72 491 2976 1 492 6389 17 492 9440 18 493 4752 40 493 589 87 493 7560 22 493 566 73 493 9979 20 493 3206 8 493 4367 6 495 4421 57 495 5978 89 496 8926 54 496 7483 9 496 5911 2 497 5319 14 497 5020 37 497 3838 76 498 7618 81 499 4362 17 499 4995 7 499 9490 67 500 171 86 501 305 35 501 22 66 501 4660 25 501 9942 33 503 9266 49 503 6443 34 504 8463 22 504 2774 96 504 1537 85 504 9503 27 504 4487 27 504 897 81 505 9089 78 506 1571 67 507 5064 3 508 6306 78 509 2701 24 510 6969 1 511 8869 5 512 8422 34 512 279 43 513 6266 78 513 1358 58 514 4308 27 514 6624 41 514 8331 90 515 4608 44 515 4494 16 516 9060 42 516 89 3 516 9192 98 516 2668 82 517 43 23 517 8470 48 518 7456 80 518 866 71 519 1502 48 520 8078 54 520 8978 53 521 8687 16 522 7317 76 524 7193 47 524 8845 76 525 9785 22 525 6313 82 526 1570 25 528 3491 11 528 9001 11 529 6256 14 530 1594 74 531 2948 49 533 1265 92 534 7771 41 534 1610 43 535 1507 83 535 8220 69 535 915 18 536 7361 50 536 4707 59 536 2644 64 538 5449 54 538 4561 31 538 5510 87 540 5073 20 541 5346 40 542 6489 92 542 1976 89 543 1220 12 544 7318 1 544 7561 32 545 7656 82 546 7390 17 546 7928 75 546 1879 45 548 4026 55 549 8081 94 550 4635 26 553 5049 64 553 5039 60 555 4862 45 556 6325 57 556 7184 77 556 5112 58 556 4946 30 557 2577 10 558 7162 52 558 9872 53 558 4483 23 559 5487 55 561 6366 46 562 9722 91 563 9148 63 563 8849 81 563 5927 1 564 2644 37 564 6858 20 564 5966 10 565 6467 50 566 7380 1 566 9102 86 566 2351 25 567 1632 11 568 8967 95 569 6099 21 570 9001 58 570 863 10 570 2257 7 570 7279 88 570 1972 27 571 5459 32 571 7895 1 572 1369 51 572 1074 59 574 1380 10 575 4433 90 575 2395 56 575 6923 58 575 4850 22 576 1666 86 577 5727 45 579 5863 46 579 4076 93 580 1426 95 580 5677 59 580 8238 68 581 1126 25 581 5140 45 581 5679 64 582 7654 76 582 6228 28 582 3788 18 582 7572 41 582 4592 91 582 7809 55 584 8532 4 585 8683 6 585 1889 89 585 8849 85 586 2450 71 586 1081 79 586 9236 94 586 3688 18 589 5853 70 589 9567 48 590 2565 97 591 4544 75 592 1359 78 592 2027 97 593 6398 71 593 6015 7 594 1615 55 594 796 68 595 3440 34 595 6764 35 595 6989 0 595 1722 30 597 8148 26 598 7732 27 601 9402 4 601 7387 72 601 8962 9 602 5662 35 602 4811 55 603 9268 27 603 2966 59 603 9152 4 605 8011 11 606 1596 1 607 8381 44 608 5563 55 608 2486 57 608 7670 9 609 7365 72 610 1839 57 610 1447 60 611 9036 58 612 1142 100 614 93 39 615 2016 60 615 9892 53 615 7532 97 616 2958 71 617 8263 19 619 9960 73 619 9757 36 620 7458 33 620 2377 47 621 6827 77 621 9929 24 621 5432 53 622 6654 88 623 7724 58 624 4481 5 624 4516 20 625 2160 36 626 9660 26 626 4162 93 628 8089 85 629 8920 9 630 8213 15 631 1382 74 633 9834 99 633 3821 31 634 6820 69 635 2532 99 636 5623 97 637 8301 57 638 5921 39 639 8353 25 639 79 61 639 8056 94 641 3559 95 642 9951 30 642 7879 8 642 3653 71 642 8835 93 643 6212 35 644 9313 60 644 3325 84 645 3216 5 646 4720 1 646 6610 24 647 6879 94 647 2375 31 648 3183 40 648 483 31 648 9295 14 648 1343 3 649 9212 95 649 241 16 649 6499 61 649 4580 93 650 1257 12 650 9348 44 651 9890 61 651 3934 71 651 457 91 652 7914 94 653 3985 8 653 7602 90 654 6304 55 654 5312 59 654 6724 2 655 3655 16 655 8232 77 655 7939 80 656 4802 32 656 6977 16 657 710 99 658 6923 63 658 5441 30 659 1821 47 660 4299 98 660 707 23 660 9619 36 661 2167 99 662 8251 31 663 346 4 663 7902 42 666 9159 27 668 1050 40 669 7348 30 669 4799 48 669 8393 71 670 6621 14 671 4371 3 671 7848 54 672 2631 41 672 6697 17 673 3233 86 673 2262 41 674 9261 38 675 2736 60 676 8967 92 676 2105 100 676 8566 51 676 1233 18 676 5229 67 676 6003 30 677 78 11 677 1654 12 678 2497 37 679 1755 91 679 3339 51 680 2058 94 681 469 46 681 717 73 681 298 33 681 6885 58 682 1887 66 682 2568 22 683 2349 22 683 4927 36 683 6818 77 684 1999 54 684 8356 84 685 709 57 685 8363 70 685 6427 68 686 4857 46 687 7202 70 687 4968 4 690 6664 56 691 5062 82 692 1520 58 692 7565 65 692 4090 55 692 4379 29 693 9397 46 697 3270 53 697 3521 87 697 8706 42 698 9667 49 698 668 96 698 1187 24 698 9364 85 699 2631 44 699 9225 68 699 4771 89 700 3444 63 701 8480 56 702 3063 52 702 6640 53 704 5756 68 704 3026 1 705 5320 83 705 6156 80 705 3903 0 706 835 7 706 7313 70 706 2424 96 707 6361 81 707 1369 62 707 2418 96 708 3684 16 709 6608 1 709 7181 30 709 2123 57 710 3316 15 710 6409 17 710 6435 73 710 7184 23 713 6266 6 713 5803 95 714 2941 81 716 6538 3 716 5603 3 717 6221 71 717 8084 18 717 2912 70 717 5696 13 719 9488 44 720 4306 51 720 2337 2 720 5225 32 720 4365 51 720 9937 100 721 357 76 722 466 46 722 1098 18 722 1866 55 723 6199 59 723 4450 79 723 3212 14 724 2326 67 725 9484 88 725 3712 100 726 678 31 726 8858 45 728 8265 19 728 7307 41 729 8018 40 730 4598 52 730 2309 27 730 4427 22 730 696 28 730 5265 83 731 8473 13 732 1856 100 733 2314 10 734 8411 22 734 2412 39 734 553 49 735 8032 16 736 930 74 737 3047 6 737 5845 5 739 2118 48 740 8879 21 741 4275 69 741 9233 77 741 9341 74 742 6764 1 742 8942 53 743 8942 42 744 6083 78 744 391 25 744 8702 2 745 5282 7 745 3502 5 745 7791 3 746 9463 6 746 3923 15 746 3397 25 747 7819 0 747 1868 35 747 2477 59 748 5294 76 748 2155 4 748 8931 30 750 7879 20 750 6613 41 751 1018 17 751 2594 51 751 8938 13 754 8688 60 754 9666 51 756 1570 41 757 9581 26 758 9616 32 758 1183 71 758 4695 77 759 5119 36 759 9155 91 760 8680 81 761 2137 49 762 9051 9 762 5278 66 765 4368 14 768 8521 39 768 8961 62 770 6448 8 772 9886 11 772 6687 74 772 9544 28 772 9477 76 774 6262 98 774 4738 75 775 2395 7 776 7123 24 776 9052 26 778 3729 66 778 5179 58 778 3438 76 779 7257 40 779 4873 11 779 8750 44 780 623 68 780 6187 64 781 8789 61 782 745 15 782 1902 9 782 3284 69 782 1466 2 782 4624 23 784 6659 2 784 4255 8 785 7389 71 785 7241 7 785 1729 32 786 7153 11 786 2174 55 786 1531 40 788 584 79 788 5189 78 788 8856 31 789 8363 40 790 1598 23 790 4169 91 791 7471 75 791 7701 43 791 8170 63 793 7943 14 794 3217 99 795 582 76 795 4293 68 795 6717 26 796 2952 98 797 3072 67 798 82 78 798 9817 100 799 231 74 799 4496 19 800 236 89 800 6846 3 801 8219 67 801 8392 52 803 6182 65 803 3933 35 805 3640 34 806 876 2 806 2574 71 806 3145 14 806 3626 23 806 8433 10 806 190 32 806 7244 79 808 9090 77 809 2753 28 809 5058 70 810 8100 15 810 6104 11 811 3750 44 812 6443 48 812 2987 11 812 2678 5 812 6969 18 812 3504 53 813 3518 38 814 5454 52 814 1864 41 816 9848 63 817 8866 18 818 6273 66 818 8739 23 818 9508 78 819 8736 50 820 5611 55 821 6747 72 823 3123 84 824 4435 90 825 2029 28 825 4116 71 825 3150 97 826 5516 4 826 2559 51 828 127 46 828 5828 25 828 8146 26 828 5113 80 829 863 77 829 63 67 831 2625 73 832 9275 28 832 9323 19 834 4558 12 835 9878 14 836 6514 14 839 3999 46 839 3864 87 839 7519 73 839 6001 60 841 7448 81 842 3529 5 842 6144 81 842 8434 56 842 7293 61 843 1000 10 843 6260 37 845 4901 27 846 7524 36 846 9112 23 846 5782 28 846 2033 93 846 5337 52 847 9210 79 848 1731 55 850 5212 34 852 6243 87 852 7679 39 853 6878 38 853 8287 95 853 7317 68 855 6576 60 855 4638 66 855 4418 85 856 9305 38 857 4414 64 858 6762 41 860 8576 72 860 2592 68 861 3579 38 862 3892 1 864 8753 86 866 6570 8 867 2617 66 867 844 55 868 9565 55 868 3983 58 868 4718 56 868 7777 9 869 5324 53 869 1984 19 870 1976 14 870 7197 48 871 7414 76 871 7131 23 872 6041 40 874 7521 35 874 7675 61 875 3639 58 875 3173 26 875 3068 22 875 6492 36 875 1797 64 878 1052 55 879 4369 8 879 5366 96 880 4399 13 880 4144 100 883 5699 5 884 7626 51 886 1513 81 886 4293 43 887 4414 49 888 3674 93 888 9377 93 889 1879 81 889 3266 86 889 57 38 890 8580 37 890 1637 54 891 766 93 891 3151 38 891 805 40 891 4475 60 892 2845 62 893 8036 45 894 9633 61 894 657 58 895 818 57 895 1142 38 896 7213 32 896 4678 6 896 1465 9 897 4121 50 897 2821 58 897 8352 71 898 9847 90 898 8581 41 898 323 76 898 4887 27 899 6437 68 899 9007 12 900 2555 37 900 3232 70 901 5359 84 901 5471 94 901 5803 46 902 3101 19 903 3424 1 903 1866 62 904 4139 66 905 1726 77 906 4787 60 906 9001 64 906 1027 50 908 9476 60 909 7954 73 910 9093 75 911 1401 87 911 8459 2 912 6784 5 913 3770 97 913 2231 20 914 1422 81 915 4445 79 916 506 21 917 4614 99 918 1838 50 918 2806 33 919 7499 84 920 3725 94 920 8666 56 921 4799 69 923 8486 40 924 9827 97 925 4473 98 925 9625 50 926 5774 96 927 2528 3 928 9285 76 928 5679 44 929 9278 80 929 5447 11 930 7408 1 930 5544 83 931 8231 27 931 7840 56 932 6249 31 932 911 42 933 6732 21 934 6658 44 935 1193 8 935 5485 32 936 5527 5 938 2444 45 938 3031 50 938 4410 70 938 2881 1 939 3905 95 939 1425 73 940 1441 12 940 9903 33 941 9173 3 942 9938 39 942 5806 53 943 8618 70 945 8275 34 945 4891 64 945 9170 12 945 8928 75 946 6626 35 947 7163 82 947 4438 80 947 6289 53 947 5322 69 949 8233 6 950 1676 99 950 975 11 950 829 14 950 7972 41 951 2922 23 952 8888 76 953 3194 54 953 6734 22 957 1983 94 959 9332 55 959 1740 39 960 8244 31 961 7554 39 962 2031 72 962 6375 10 963 6775 69 963 8300 67 963 6608 55 965 9946 67 966 2532 94 967 3475 35 967 1671 94 968 6227 40 968 3496 70 969 5420 15 969 3139 8 970 621 9 972 9761 18 972 6263 34 973 7980 22 973 8480 61 975 7480 23 975 854 54 975 316 99 976 2425 21 976 5299 87 977 6652 60 977 2533 5 978 4747 92 978 4854 26 979 4172 80 979 6180 81 980 6376 28 981 7924 35 982 3154 15 983 8812 53 984 3133 88 984 2934 58 984 9202 50 985 6811 63 985 8958 8 986 8238 55 987 1093 96 989 8813 4 990 5445 62 991 3522 96 991 6723 28 991 454 95 992 9845 47 993 9402 24 995 3011 0 997 5493 54 997 8955 83 998 4371 85 998 2277 43 999 7300 97 1001 5022 25 1002 1765 86 1002 8695 33 1003 1390 60 1004 9498 44 1004 1309 66 1004 4445 24 1005 9325 80 1005 3026 15 1006 9418 2 1006 1047 69 1007 813 5 1007 9540 56 1011 7840 8 1011 7455 38 1012 7639 60 1012 3245 35 1013 7892 64 1013 4580 67 1014 5080 48 1014 8480 34 1017 6719 24 1017 1155 87 1018 7720 45 1018 3716 45 1019 201 44 1021 6217 47 1021 7382 68 1022 7567 39 1022 7988 34 1024 1781 26 1024 6304 9 1025 3372 41 1026 9675 90 1026 542 87 1027 1808 100 1028 800 33 1028 6093 20 1029 6773 49 1030 9667 21 1030 6356 91 1032 9740 85 1032 6390 39 1032 1384 23 1033 2459 63 1034 8117 95 1034 6520 20 1034 5155 79 1034 4335 99 1035 5160 18 1036 1357 40 1038 2436 67 1038 5497 61 1039 3128 99 1039 9337 46 1040 8253 39 1040 3961 0 1040 2674 90 1040 7423 42 1041 4977 90 1041 2705 45 1042 6869 9 1042 3380 49 1044 368 36 1044 1174 26 1044 3651 49 1045 8436 66 1045 6635 56 1045 547 4 1046 2363 35 1048 2637 99 1049 5920 90 1050 4150 44 1052 8897 36 1052 5952 49 1053 6874 44 1053 438 60 1054 5352 13 1054 5737 31 1055 457 55 1055 4233 43 1057 2919 97 1057 399 97 1058 8433 37 1058 3863 84 1058 1066 8 1059 2453 29 1060 1796 9 1060 1307 87 1062 508 1 1062 1427 50 1064 9080 61 1065 4662 99 1065 9849 43 1065 9348 20 1066 7093 61 1066 5715 40 1066 7722 14 1067 2858 77 1068 1808 32 1068 4150 78 1069 7494 55 1070 9963 95 1070 8803 77 1072 3221 73 1072 6158 91 1076 8465 18 1077 809 35 1077 8715 11 1078 239 5 1078 2888 95 1078 678 62 1079 4573 64 1079 5273 39 1080 9863 27 1080 4313 31 1081 2719 44 1083 5182 21 1083 9203 71 1086 2472 14 1087 8650 32 1087 7590 53 1087 7047 96 1087 4848 98 1088 3277 70 1089 3080 91 1089 9168 86 1091 5901 17 1091 1066 66 1092 6340 28 1092 716 79 1092 8007 86 1093 9731 28 1093 7659 73 1093 5463 92 1093 6036 27 1093 866 29 1094 4129 29 1095 7176 36 1095 4336 91 1095 1302 1 1096 3368 12 1096 2006 67 1096 2119 61 1096 5771 54 1096 3527 66 1098 9065 5 1099 2068 43 1099 1812 7 1099 8155 31 1101 3937 33 1101 1136 52 1102 4533 9 1102 9329 11 1103 5258 30 1103 7522 35 1103 9677 33 1104 8386 60 1104 6506 17 1104 8377 7 1105 8219 32 1106 9172 16 1107 5587 82 1107 5144 55 1107 5022 100 1108 5765 26 1109 3477 90 1109 3471 74 1109 5600 7 1109 8419 99 1110 4883 79 1110 5543 24 1111 4207 14 1112 664 82 1113 7649 54 1113 7118 43 1113 8485 98 1114 2246 92 1114 1893 85 1115 2208 54 1116 8726 96 1116 8679 18 1116 1593 22 1116 9350 86 1117 8381 51 1117 3512 76 1117 3666 59 1118 9239 100 1118 3104 37 1119 9751 3 1120 5062 53 1120 5918 47 1120 9218 29 1120 9558 47 1121 5123 76 1121 9670 91 1122 4473 49 1122 8983 79 1122 9079 42 1123 9011 54 1123 5128 52 1123 6214 23 1123 6062 86 1124 4365 40 1124 7008 90 1125 3351 5 1125 3824 62 1126 7111 100 1127 5230 0 1127 3244 33 1128 1028 24 1128 9741 31 1128 818 77 1130 6090 100 1130 1378 15 1130 6699 73 1133 1687 10 1134 8176 13 1134 6510 55 1135 7877 22 1135 5721 66 1137 5687 23 1137 4914 17 1138 2154 19 1139 8258 7 1139 920 98 1139 3926 35 1139 6119 66 1141 6849 86 1142 9737 41 1142 6155 38 1142 4613 74 1144 9875 13 1145 6380 36 1145 3144 73 1145 8376 78 1146 7377 51 1146 843 28 1147 1993 21 1147 2889 9 1148 2124 95 1148 406 75 1148 9032 33 1148 3689 27 1148 723 65 1149 3688 29 1151 2256 32 1152 154 29 1152 1550 50 1153 9962 98 1153 7552 5 1153 4399 27 1153 6848 68 1153 5994 9 1154 5161 42 1154 701 81 1154 7004 55 1155 94 84 1157 8755 69 1157 2018 6 1158 7145 67 1158 2 63 1159 4595 79 1160 8149 62 1160 5267 92 1160 2941 71 1160 6212 94 1162 3181 21 1162 3041 91 1163 7834 1 1163 2301 52 1163 6671 39 1164 4738 38 1166 6521 12 1167 8881 50 1168 5719 83 1169 3098 80 1169 9060 60 1170 1833 82 1171 8382 90 1172 6197 35 1174 3923 64 1174 4632 63 1174 8426 80 1175 3875 46 1175 3845 33 1176 3811 49 1177 4334 27 1177 6551 27 1178 7915 0 1179 9731 50 1179 1381 95 1179 6613 34 1180 9598 59 1181 7245 51 1182 8824 47 1184 4860 69 1185 5705 99 1186 2570 47 1186 4172 97 1186 1495 38 1188 745 25 1191 6773 5 1191 750 60 1191 9535 5 1192 9998 8 1192 317 76 1193 9353 43 1194 9365 6 1194 6267 55 1195 7251 27 1195 7099 54 1195 6360 99 1196 4796 73 1197 5628 50 1198 7043 61 1199 4947 46 1199 6807 42 1199 6156 79 1200 8337 79 1200 6712 88 1201 9704 1 1202 233 76 1202 3521 74 1203 6591 57 1206 9005 34 1206 7113 3 1206 988 16 1207 4171 9 1208 1675 46 1209 9024 41 1209 6742 49 1209 8242 50 1210 3035 68 1211 9249 8 1213 1560 70 1213 3226 9 1214 9843 54 1214 742 82 1215 4677 23 1215 5528 29 1215 5359 30 1216 6756 35 1216 3921 58 1218 2161 28 1219 8079 81 1219 7721 13 1220 6312 15 1220 5188 8 1220 837 9 1221 1565 14 1221 6929 55 1225 8658 81 1227 6409 34 1227 3548 45 1227 1466 66 1227 9620 40 1230 4467 70 1231 4654 0 1231 6991 79 1231 7138 3 1232 1893 39 1233 6082 98 1234 9504 64 1235 506 47 1235 5219 70 1235 2225 48 1236 5267 65 1236 9577 89 1237 6781 23 1239 7824 42 1239 3569 94 1241 8366 26 1241 4877 77 1242 6805 2 1242 3569 99 1242 3633 26 1243 3550 34 1244 7108 82 1245 6587 46 1247 6944 97 1247 5490 25 1249 1090 57 1250 835 49 1250 2965 22 1251 5346 38 1251 7395 81 1252 3613 68 1252 4319 78 1256 1149 67 1258 3964 80 1258 5302 7 1260 3634 36 1260 1441 78 1260 7981 59 1261 365 37 1262 3288 41 1263 7662 65 1267 3668 86 1267 2744 81 1268 9006 93 1268 6595 47 1268 8849 2 1270 2869 20 1270 7748 8 1270 7912 4 1270 6163 58 1271 8622 38 1272 8540 100 1272 2353 5 1273 6604 64 1273 5388 58 1274 7257 74 1274 3156 85 1274 7291 96 1274 364 26 1274 3113 10 1275 3171 5 1275 8617 70 1275 852 89 1275 9078 56 1275 4251 57 1276 2089 46 1276 2639 0 1277 1507 64 1279 6637 91 1279 9535 84 1279 7574 29 1280 8838 4 1280 9683 7 1280 1376 30 1280 5855 19 1280 852 68 1281 7417 65 1281 1573 89 1282 2706 25 1283 5400 93 1284 9677 51 1285 721 28 1285 5051 18 1285 997 82 1285 4081 3 1287 705 48 1288 2685 2 1288 2571 53 1288 492 8 1288 8589 31 1288 6909 51 1288 5820 29 1289 2837 14 1291 988 1 1292 9130 74 1292 2141 10 1292 5519 99 1292 2771 8 1293 4787 68 1293 6248 1 1294 1967 70 1294 4993 97 1294 8270 36 1295 4825 58 1296 3668 77 1297 6628 4 1298 4794 68 1299 9042 1 1302 8891 62 1302 3067 64 1302 6870 49 1303 4408 46 1303 7625 70 1304 6449 86 1304 3498 50 1304 4854 2 1306 3563 53 1307 5440 65 1307 5266 91 1308 844 65 1308 5450 9 1308 1663 11 1309 7273 25 1310 733 35 1310 5516 35 1312 5858 0 1313 3486 4 1313 859 13 1314 5073 39 1314 6307 57 1314 8682 74 1315 4769 24 1316 7946 13 1317 7020 8 1317 5890 8 1318 7996 40 1318 2200 90 1318 5982 29 1320 9547 6 1320 750 6 1320 3017 18 1321 4673 37 1323 8181 38 1324 9001 13 1324 2288 81 1324 4789 62 1324 6597 9 1324 2886 22 1324 8854 77 1325 3072 48 1326 3883 85 1326 8546 56 1327 7956 24 1329 2471 97 1330 3280 42 1331 2971 24 1333 4194 74 1333 4499 51 1335 8831 77 1336 2890 13 1336 7146 58 1338 3115 98 1338 8184 71 1338 7749 71 1340 7946 16 1341 8223 7 1345 5315 0 1345 1144 70 1346 8232 39 1346 4057 50 1346 7702 24 1346 8795 92 1347 5366 13 1349 7496 75 1350 3887 57 1351 7250 78 1351 6265 20 1352 3229 14 1353 4527 68 1356 4478 13 1356 3686 56 1356 7411 70 1358 4719 36 1358 1612 79 1358 2300 73 1358 3205 48 1359 3799 57 1359 4368 100 1361 4579 59 1361 9495 46 1361 2335 26 1363 7317 92 1363 3141 67 1364 3821 47 1364 3297 93 1365 4252 15 1366 2190 0 1366 1773 72 1367 433 74 1367 2707 80 1367 8179 93 1369 778 67 1371 8107 4 1371 8328 3 1371 4515 89 1371 1721 79 1372 7202 72 1372 3178 4 1373 3043 62 1375 4470 22 1375 5374 94 1375 8568 46 1377 5657 16 1377 1586 62 1378 6204 97 1381 2795 8 1381 3286 92 1383 1277 73 1384 1024 62 1385 1821 73 1385 3614 92 1385 9012 86 1387 4816 36 1388 2686 81 1388 6997 44 1388 6320 61 1388 1101 55 1389 7209 36 1390 5059 25 1390 2842 18 1391 8262 78 1392 2722 13 1395 6113 40 1395 3284 47 1397 1193 69 1397 3965 74 1397 4137 86 1398 2082 61 1400 8136 60 1400 7004 95 1401 6939 45 1401 3831 9 1402 6480 70 1402 4189 90 1403 2343 15 1403 8712 3 1404 108 56 1404 6547 6 1404 5889 30 1404 1310 68 1405 824 87 1405 2710 39 1406 1226 99 1406 6627 34 1407 3721 44 1409 922 42 1409 8235 66 1409 5398 46 1409 8315 21 1410 6901 49 1411 6052 14 1411 5807 75 1412 4306 28 1413 7145 99 1414 2245 82 1415 770 54 1415 1690 64 1417 5995 77 1417 7507 49 1417 7516 70 1418 4427 30 1419 9382 55 1420 473 34 1421 9848 52 1421 5209 11 1423 5370 73 1423 8571 55 1424 5028 88 1426 7165 100 1427 6706 2 1428 9160 64 1429 2977 48 1429 7997 65 1430 4922 93 1430 6419 14 1431 4852 79 1431 5989 38 1432 7097 18 1432 8497 58 1432 8037 75 1432 917 13 1433 1628 10 1433 5177 47 1433 1477 61 1433 3561 53 1433 1377 92 1435 4025 9 1435 8850 12 1435 6518 42 1435 5937 49 1436 3173 94 1436 3574 1 1437 7279 54 1437 2199 90 1438 9707 55 1438 7102 58 1438 9853 75 1439 7237 60 1440 5684 20 1440 6975 77 1440 3398 42 1440 4765 12 1441 6276 6 1441 4353 63 1442 4615 54 1443 2250 30 1443 9929 73 1444 5814 8 1445 7102 3 1446 4287 71 1447 246 55 1448 2182 4 1449 8037 25 1449 4701 51 1449 5399 15 1449 3395 65 1450 6808 74 1450 5628 54 1451 7556 93 1451 791 44 1452 9075 45 1452 4710 23 1453 6555 73 1454 5335 3 1454 6095 40 1454 1482 49 1455 31 30 1455 5214 61 1455 6304 4 1456 3991 29 1456 2899 18 1457 4951 97 1457 4637 75 1458 698 25 1459 7171 17 1460 5404 87 1460 8987 25 1462 5058 89 1462 7600 57 1462 3298 79 1465 2342 43 1465 8528 96 1465 3251 6 1466 3360 84 1467 677 68 1467 9471 38 1467 3222 95 1468 1881 78 1468 2315 98 1468 796 18 1469 9466 2 1469 4787 24 1470 3896 41 1471 7877 15 1471 5113 89 1472 9812 18 1472 68 30 1472 8386 41 1473 8463 92 1474 2700 21 1474 2488 79 1477 145 28 1477 9619 89 1477 5960 55 1478 8228 31 1479 2131 21 1480 6791 38 1481 1632 13 1483 9708 55 1483 3648 56 1484 1847 62 1484 6249 25 1485 5656 18 1487 7107 56 1487 5098 31 1488 5477 95 1489 5691 62 1489 5090 36 1490 9893 45 1492 3489 5 1493 1769 81 1493 6823 46 1497 4756 100 1497 906 63 1498 5704 80 1498 8547 47 1498 5616 42 1499 8444 13 1500 1893 99 1501 1058 3 1501 2293 25 1501 4366 62 1502 4400 98 1502 9357 11 1503 7841 17 1503 4647 1 1503 6113 9 1504 8898 54 1506 1817 39 1511 7532 40 1512 5144 38 1512 2743 61 1512 6443 78 1512 5373 58 1512 1692 48 1513 1056 31 1513 7874 94 1514 8797 87 1514 6650 55 1515 144 34 1516 8356 37 1516 7313 89 1517 6001 10 1517 2273 92 1519 4815 5 1519 3915 63 1520 7294 87 1521 8295 58 1521 6626 3 1522 6120 90 1524 8035 34 1525 4879 94 1525 3363 60 1525 8219 16 1527 5437 61 1529 4227 94 1529 7767 11 1529 7542 73 1530 7092 16 1530 5086 36 1530 6195 0 1532 1421 55 1532 6505 36 1532 6647 96 1535 9933 38 1539 3051 31 1539 8658 40 1539 7670 19 1541 641 15 1543 7507 50 1543 9145 48 1543 6912 57 1543 4612 38 1544 3914 82 1546 9078 28 1546 2219 95 1546 6324 16 1547 2373 90 1547 3249 3 1548 7265 59 1548 8632 4 1549 1500 98 1549 2258 94 1550 9047 69 1551 3584 56 1552 2962 61 1552 7530 35 1553 6371 86 1553 7826 6 1554 3923 56 1554 5079 47 1554 6211 50 1555 1289 22 1556 9603 33 1557 841 94 1557 4142 94 1558 5208 80 1559 3230 41 1560 3917 10 1560 7270 54 1560 2165 55 1561 2452 17 1561 4907 31 1561 473 31 1561 6167 70 1563 3619 29 1563 1640 4 1563 6923 86 1564 4309 71 1564 400 60 1564 1544 96 1564 5005 97 1565 4217 44 1566 8958 85 1566 981 27 1566 3477 72 1566 9938 46 1566 5006 35 1566 335 83 1567 5937 84 1568 4249 68 1573 9652 31 1574 9255 33 1574 6634 65 1575 5898 96 1575 971 55 1576 7250 18 1576 4784 51 1576 4248 42 1579 9930 53 1579 7821 46 1580 7726 16 1580 8435 39 1580 106 13 1581 7888 7 1582 9961 47 1582 8267 92 1582 3480 1 1582 4211 98 1582 9794 19 1584 6566 47 1584 5110 55 1584 6634 31 1585 4560 57 1585 6036 18 1586 790 17 1587 3928 51 1587 3379 98 1588 6041 48 1590 1950 43 1594 2351 86 1595 8107 18 1595 7373 54 1596 8134 7 1596 5681 9 1596 7044 2 1597 9402 14 1597 8426 59 1597 6278 100 1597 1136 69 1597 3196 86 1599 1141 96 1602 4826 91 1604 7165 98 1605 2092 41 1605 3383 70 1606 7696 12 1606 3939 0 1607 4825 59 1607 5980 18 1608 4431 71 1608 2097 86 1610 3304 87 1610 497 68 1610 4568 73 1610 5275 56 1611 7089 20 1612 5697 83 1612 7968 14 1613 6154 23 1614 4544 1 1614 7900 71 1614 9107 65 1616 6448 38 1617 7231 45 1618 4787 47 1618 4797 57 1619 4135 42 1620 4099 77 1620 3351 14 1621 2421 25 1623 1209 49 1623 4113 34 1624 4206 99 1624 8783 45 1624 9828 60 1625 244 26 1628 6786 11 1629 9069 79 1631 8584 72 1631 539 33 1632 2401 96 1633 6623 54 1633 1092 18 1634 3000 84 1634 6891 67 1635 4672 31 1635 2852 62 1635 5509 100 1635 2665 25 1636 7711 34 1636 949 12 1636 7841 7 1637 334 42 1637 5558 94 1638 6960 8 1638 7966 41 1638 5176 52 1638 5924 7 1639 6353 80 1642 2913 36 1643 100 100 1643 1025 43 1643 9175 27 1644 1751 7 1644 8067 53 1644 4906 80 1645 6415 100 1645 3202 34 1647 756 17 1648 7879 28 1648 9738 44 1649 3459 57 1649 6352 49 1649 9096 63 1649 5293 81 1649 1032 14 1650 9988 93 1651 55 92 1651 878 4 1651 6037 74 1651 6386 47 1652 3513 71 1654 80 39 1654 5380 39 1654 1213 12 1654 9210 35 1655 1223 100 1656 7036 37 1656 3992 38 1656 2735 91 1657 1351 54 1657 9068 90 1657 5621 55 1659 1521 10 1659 4482 76 1659 8013 48 1661 3430 73 1661 3609 70 1662 5943 25 1662 112 98 1663 4679 13 1664 9341 58 1664 2656 11 1664 4520 70 1664 1769 24 1665 6137 0 1665 5200 99 1665 8424 77 1666 9371 5 1666 5196 32 1666 3829 79 1666 4104 8 1666 4707 74 1666 8515 46 1667 791 16 1668 9035 3 1668 2663 66 1669 2529 13 1669 1456 21 1670 7342 49 1671 1619 97 1671 610 29 1673 4852 100 1673 8576 1 1673 4962 23 1674 6581 59 1674 7453 7 1675 2781 53 1675 1314 73 1676 1005 94 1676 1639 46 1676 75 61 1678 6199 1 1678 7635 95 1678 8789 14 1678 5883 52 1678 7919 80 1679 9520 0 1681 9038 3 1682 715 18 1682 9835 31 1682 2119 53 1682 1696 46 1682 690 84 1682 5205 10 1683 3126 73 1684 8850 21 1684 4089 13 1685 5544 2 1687 3879 18 1688 2857 40 1689 3540 73 1689 634 43 1690 944 78 1690 4214 72 1691 1155 15 1691 8431 21 1692 7409 99 1692 4571 91 1694 8192 70 1695 9102 81 1695 5274 85 1696 3259 41 1697 9424 76 1697 1510 49 1698 9428 44 1698 630 89 1698 2963 93 1699 7591 82 1700 716 42 1700 7585 67 1701 4273 90 1702 4509 33 1702 918 15 1703 6855 34 1704 9818 58 1704 8124 62 1706 2546 61 1706 8453 86 1709 9338 17 1711 8199 39 1713 8832 13 1713 2260 80 1713 26 73 1714 5358 23 1715 8887 63 1715 8835 56 1716 2469 96 1717 3681 19 1718 6225 78 1718 438 63 1718 7488 72 1719 3081 37 1719 6416 63 1720 6063 23 1720 3430 32 1720 5802 53 1723 5067 41 1723 7637 22 1724 7783 48 1726 8297 2 1726 393 61 1727 2923 90 1727 8104 50 1727 4092 27 1727 6297 20 1729 662 33 1733 3357 62 1733 2929 12 1734 5969 38 1734 308 81 1734 4222 30 1735 4910 25 1737 3865 69 1737 6430 37 1739 6309 93 1739 1735 67 1741 8305 6 1742 7866 21 1742 1036 94 1744 735 16 1744 1047 78 1745 9294 82 1745 8525 47 1747 6493 71 1747 8855 53 1748 845 76 1749 607 62 1750 4282 10 1751 8095 63 1753 4327 49 1753 5338 5 1754 9396 79 1756 9277 31 1756 647 96 1756 9192 46 1757 9341 1 1758 3920 97 1758 5309 45 1758 3947 56 1758 4809 60 1759 6061 9 1759 3214 60 1761 2761 80 1763 8360 7 1763 5792 32 1764 5016 7 1765 6320 8 1765 8912 34 1766 5359 8 1768 6731 81 1769 6614 19 1769 5053 33 1770 9217 51 1771 8625 6 1772 4045 69 1772 5308 89 1773 9211 62 1773 832 30 1774 2336 61 1774 9629 6 1775 1952 97 1776 1938 82 1777 826 23 1777 5108 0 1777 6166 14 1781 398 84 1782 6008 3 1782 1764 89 1782 2937 64 1782 9754 18 1783 123 77 1783 7900 60 1783 9650 38 1784 4730 50 1785 6071 15 1785 2350 80 1786 9870 18 1786 1464 27 1787 9853 75 1788 7716 9 1788 4222 51 1788 3194 31 1791 5876 90 1793 1177 79 1794 2093 12 1795 243 99 1796 3893 95 1796 5513 63 1797 9691 82 1797 6576 85 1797 3569 77 1797 9071 95 1797 5986 34 1798 5674 59 1798 7986 40 1799 3155 87 1799 4953 82 1800 7731 66 1802 6059 28 1807 6219 92 1807 2816 27 1808 6389 68 1808 9638 56 1809 9769 86 1809 7903 94 1809 2912 36 1809 472 21 1809 9849 98 1811 671 78 1812 5761 54 1812 2933 68 1815 1768 30 1816 3246 1 1817 918 22 1817 167 90 1818 6905 55 1820 7196 59 1820 9658 97 1820 61 40 1821 5421 85 1821 4791 30 1823 9318 76 1823 2865 25 1824 2831 81 1824 8358 73 1824 2786 29 1824 6046 28 1825 1698 46 1825 1721 72 1826 2628 0 1827 5909 64 1827 5150 25 1828 257 56 1828 6828 63 1828 2053 77 1830 584 3 1830 5326 96 1831 2892 31 1832 9168 88 1832 8981 32 1833 6933 10 1833 6666 94 1835 6456 53 1835 5200 44 1837 6674 58 1837 3887 89 1837 833 26 1837 2374 62 1838 569 35 1839 8642 68 1841 8960 92 1841 3638 38 1843 6818 100 1844 7670 96 1847 2142 12 1848 9809 50 1849 1812 49 1850 8381 99 1851 4999 96 1851 4559 40 1851 9921 4 1852 7890 19 1853 2395 53 1853 1628 13 1854 9187 97 1854 9009 15 1855 4993 55 1855 5528 12 1855 2962 97 1855 2896 57 1856 5961 100 1856 4013 58 1856 2131 66 1857 9875 26 1859 8338 31 1859 1607 13 1859 4026 31 1860 5952 28 1864 4826 49 1865 1062 79 1865 223 81 1866 4023 74 1866 3193 94 1867 1005 43 1867 9129 39 1867 8671 50 1867 7037 63 1868 3774 40 1869 6217 12 1870 656 11 1871 5558 28 1872 4206 26 1873 7180 88 1874 8121 39 1874 3392 30 1874 2924 100 1874 6039 74 1875 6488 11 1875 2438 15 1876 8382 95 1877 1098 91 1878 1950 47 1878 5743 78 1879 2380 84 1881 1926 82 1881 1977 72 1882 6746 55 1883 8267 63 1884 1105 36 1884 6124 38 1884 1072 1 1884 5978 20 1885 362 61 1885 6712 78 1886 5574 23 1887 4742 10 1889 6328 51 1894 6227 41 1895 7476 38 1895 3797 30 1895 2343 78 1896 39 81 1897 681 85 1898 4460 8 1899 6075 18 1899 806 99 1899 5149 55 1900 623 37 1900 7959 42 1902 335 77 1903 7394 64 1904 7726 19 1905 6407 94 1905 4019 38 1905 6222 76 1906 151 0 1906 9128 52 1907 1537 20 1907 2430 69 1907 7001 93 1908 7235 9 1909 4242 15 1909 9794 58 1910 7731 63 1911 4293 57 1911 3580 96 1912 139 52 1913 6023 19 1914 5143 83 1914 4661 14 1915 6646 37 1915 7462 91 1915 3813 1 1916 3365 36 1917 4154 53 1918 1063 81 1918 6427 3 1919 1862 4 1920 8039 13 1920 8626 58 1921 352 44 1922 7378 91 1922 3489 17 1922 9580 33 1922 6501 29 1922 9939 41 1923 7328 74 1924 4835 27 1926 1308 65 1926 139 30 1927 2168 47 1927 4184 75 1927 8679 39 1927 9162 42 1928 8174 39 1930 2300 12 1930 7705 19 1931 2276 60 1931 4172 20 1931 8161 43 1932 7782 22 1933 3356 82 1933 2 61 1934 5677 83 1935 7975 67 1935 9556 5 1938 8187 69 1938 3047 1 1938 9618 57 1939 4030 19 1940 278 73 1940 4554 30 1940 7189 16 1941 9429 90 1941 2133 44 1942 2188 92 1944 5566 62 1944 9508 74 1945 8571 63 1945 3664 83 1945 348 24 1945 6882 92 1945 5761 87 1946 6429 78 1946 3933 67 1946 7888 15 1948 2881 89 1948 6178 29 1948 6045 34 1951 883 5 1953 6853 16 1954 2394 73 1954 4820 31 1954 8545 79 1955 7279 67 1955 7272 1 1956 5159 97 1956 5715 14 1956 8628 26 1956 1481 25 1957 4917 9 1959 4699 58 1962 6044 12 1963 770 0 1966 4173 67 1966 2617 99 1966 9980 85 1967 2337 60 1968 3570 17 1969 1324 79 1969 5858 40 1970 1580 58 1970 7230 96 1970 445 65 1971 4599 59 1972 7727 17 1973 833 57 1973 9756 16 1974 960 41 1974 4745 53 1974 5774 82 1976 1575 15 1977 5177 63 1978 6634 47 1978 3809 98 1979 5464 58 1981 1631 50 1981 4010 62 1985 6057 56 1985 3022 7 1986 3487 80 1986 3887 82 1986 2817 82 1986 7543 50 1987 6842 2 1988 7928 78 1989 2495 4 1990 5181 86 1991 5031 74 1992 6059 60 1993 9053 52 1993 8725 24 1994 8639 33 1996 428 12 1996 1159 43 1997 1847 46 1997 5323 80 1997 6892 53 1997 2516 66 1998 3185 93 2000 5481 54 2000 8688 88 2001 4793 44 2001 1606 96 2001 8500 99 2003 6135 75 2005 1971 33 2005 6718 21 2005 5756 46 2006 7737 24 2006 4650 15 2006 98 55 2007 922 78 2007 9755 81 2008 7559 71 2009 8159 50 2009 4430 27 2009 8955 93 2010 1928 76 2010 7980 67 2010 519 81 2010 493 65 2011 7694 96 2011 6464 55 2011 3977 81 2011 773 95 2011 853 18 2012 2592 70 2012 7841 2 2012 4813 91 2013 535 69 2013 541 97 2014 9521 60 2015 2120 93 2017 4698 73 2018 3886 24 2018 5026 18 2019 4806 50 2020 3372 44 2020 7017 49 2020 7505 71 2021 2076 21 2021 7873 7 2022 1163 55 2024 6060 75 2024 9155 12 2025 3377 80 2025 8610 56 2027 1434 81 2027 8312 98 2027 8300 66 2028 5156 77 2029 3927 42 2029 2814 73 2032 4195 63 2032 103 20 2033 1036 87 2033 9878 22 2033 9695 36 2034 1824 4 2035 2800 44 2037 6807 39 2037 4068 30 2037 3827 20 2041 1999 91 2041 3893 22 2042 3819 50 2043 3849 42 2043 9632 20 2044 3751 87 2044 2048 53 2044 9041 87 2045 9739 49 2045 2648 84 2046 5782 27 2046 2513 71 2047 7766 14 2048 2616 43 2048 6653 73 2050 14 14 2050 4524 12 2051 310 54 2052 8787 5 2052 3052 36 2052 8618 31 2052 3695 10 2053 810 76 2053 7680 3 2053 2603 11 2054 7168 98 2054 8259 30 2054 199 71 2054 6725 99 2055 4602 89 2056 6987 61 2056 3958 78 2057 7780 82 2060 9947 21 2061 4934 68 2062 2225 4 2063 7940 23 2063 5220 84 2063 6119 96 2063 9509 99 2064 1323 99 2064 6895 82 2065 5051 41 2067 2087 53 2067 7355 82 2068 9265 51 2069 2706 9 2071 9880 77 2071 8824 20 2072 7479 85 2073 8095 1 2073 6757 10 2074 8385 3 2074 593 46 2074 7132 31 2075 3700 64 2077 7849 84 2077 1459 29 2079 5810 68 2079 9959 81 2079 6638 35 2080 2542 3 2081 6514 5 2081 7930 72 2082 3109 82 2082 736 41 2084 3564 29 2084 6287 28 2084 4780 79 2085 603 28 2085 2803 95 2086 5639 99 2086 7146 49 2087 8886 9 2087 8786 25 2087 6625 75 2088 8789 13 2088 3492 83 2091 1271 8 2092 4155 33 2092 524 72 2092 535 79 2094 8468 14 2094 1345 46 2095 3797 54 2095 1621 47 2095 4808 9 2095 237 86 2095 9083 35 2095 6392 72 2096 7936 73 2096 9194 74 2098 1296 40 2102 8732 40 2102 8795 9 2102 6401 72 2103 888 63 2104 4826 55 2106 6846 10 2106 1423 48 2106 6734 36 2107 5024 89 2108 8800 57 2108 7462 96 2109 1194 5 2109 1725 38 2110 3617 98 2110 4235 10 2110 5869 38 2110 8501 88 2112 6829 65 2112 6863 63 2114 273 69 2115 9857 44 2116 542 55 2116 2499 89 2117 5522 76 2119 5973 40 2121 5982 33 2122 2890 41 2125 6023 28 2125 5031 76 2126 7343 40 2127 3850 54 2128 6877 22 2129 9475 0 2130 5899 2 2133 8201 78 2133 1792 80 2135 3669 31 2136 8587 95 2138 9746 35 2139 1476 78 2139 8298 68 2141 549 63 2141 8964 32 2141 4242 89 2142 7552 82 2143 9290 11 2143 5866 34 2143 4324 46 2143 9801 50 2144 5957 60 2144 8561 47 2148 4701 94 2150 4947 10 2151 9763 49 2153 8217 19 2153 8023 29 2153 285 94 2154 3175 68 2154 9824 12 2155 9880 73 2155 9735 33 2155 7123 72 2156 7105 5 2157 9670 64 2157 1369 56 2157 9505 88 2158 4031 89 2158 8787 5 2159 2776 32 2159 2465 43 2159 8046 2 2161 6179 15 2162 9008 7 2162 8826 99 2163 2692 87 2163 1050 94 2163 1285 40 2164 5471 91 2164 6487 73 2164 8156 6 2164 700 86 2165 2291 55 2166 7092 7 2166 1747 8 2167 1976 72 2168 4761 46 2169 9941 69 2169 8204 59 2170 7289 79 2170 8900 58 2171 5514 98 2171 2654 56 2171 4093 67 2172 681 39 2172 4462 23 2172 8354 85 2173 8677 75 2173 5742 82 2173 4676 51 2173 4725 44 2173 1489 88 2174 130 64 2177 7294 81 2178 504 49 2178 1761 66 2180 7938 71 2180 2005 99 2181 4943 86 2181 6700 27 2181 1304 48 2184 972 62 2185 6483 63 2185 3037 92 2185 7906 65 2185 4594 44 2185 2184 4 2185 376 90 2186 1451 36 2187 4231 61 2187 5020 22 2188 6030 82 2188 5506 3 2189 9619 72 2189 355 44 2190 372 7 2190 5512 59 2190 1117 68 2190 6985 20 2192 5570 46 2192 4494 43 2193 142 52 2193 5423 49 2194 8072 26 2194 8150 11 2194 3247 60 2198 9721 34 2198 3982 78 2198 7803 76 2198 2285 49 2201 483 14 2203 9676 21 2203 7798 59 2203 4327 92 2204 4873 98 2204 2520 66 2204 2804 2 2205 4952 46 2206 6159 52 2206 9346 20 2207 2170 14 2208 768 4 2209 7883 76 2210 4304 37 2210 1586 39 2211 7590 28 2211 9010 87 2212 9190 0 2212 6104 38 2212 4097 96 2213 6620 52 2213 6943 50 2215 8678 53 2215 6652 89 2215 4138 83 2216 1715 82 2216 5647 99 2216 4992 71 2217 3364 4 2217 8458 80 2219 3454 15 2219 7238 7 2219 1873 27 2220 7626 26 2220 6701 41 2220 768 68 2220 6848 9 2220 2296 52 2221 2475 71 2222 1053 24 2222 3911 11 2223 8505 7 2223 9907 89 2224 3491 83 2228 5036 39 2229 9914 82 2229 7448 83 2229 321 52 2229 1032 18 2230 430 16 2230 5623 10 2231 9943 88 2232 6145 42 2232 6336 86 2233 3586 4 2233 9900 80 2234 3339 80 2234 2854 47 2234 2787 26 2235 5221 32 2235 4518 33 2235 7550 61 2235 945 4 2236 5738 72 2236 5329 57 2237 7598 17 2237 2290 65 2238 8977 87 2238 1636 37 2239 6894 43 2240 8024 85 2240 2752 75 2241 5431 59 2242 9682 40 2242 9108 3 2243 1368 25 2244 6392 17 2244 6260 19 2244 1269 48 2246 1148 51 2246 1712 18 2246 3225 97 2248 4368 11 2249 878 33 2250 6902 8 2251 722 10 2251 822 87 2251 9208 95 2251 333 8 2251 6538 15 2254 7847 24 2255 501 26 2256 4722 29 2256 3572 25 2257 3280 66 2257 9279 14 2257 7866 0 2259 9873 96 2259 1537 74 2261 2406 62 2261 9015 66 2262 4873 24 2262 9251 9 2262 9619 77 2264 2917 78 2264 1577 81 2266 1930 75 2267 6817 76 2267 7681 26 2268 6203 17 2268 2420 71 2269 4406 45 2269 3232 93 2269 4387 62 2269 2797 37 2269 756 60 2269 1037 1 2270 3670 11 2270 3219 40 2272 5869 55 2273 8966 89 2273 2850 83 2273 8776 65 2273 2726 61 2273 4628 83 2274 3722 92 2274 4766 54 2275 8829 75 2275 5671 86 2276 5313 80 2276 8678 67 2276 8776 53 2277 2036 11 2277 7298 40 2278 8952 8 2278 8549 11 2278 8185 36 2279 8941 47 2280 4745 100 2281 5655 39 2282 9213 72 2282 5311 39 2282 1587 43 2282 1696 96 2283 7170 31 2284 6725 80 2284 8476 58 2285 230 20 2285 7028 36 2286 7714 36 2287 6279 71 2287 8520 94 2289 3118 27 2290 6807 49 2292 1018 79 2292 4487 40 2292 3565 89 2293 1050 8 2293 6961 47 2294 3054 22 2294 4694 70 2294 8948 92 2295 8562 85 2296 7341 65 2296 7337 83 2297 2728 9 2298 7991 81 2300 9206 97 2300 1006 61 2301 6332 99 2302 7209 76 2303 8020 55 2303 6686 38 2304 7130 94 2304 4434 2 2305 83 96 2308 1070 69 2309 5362 5 2309 5348 73 2309 9499 38 2311 4962 98 2312 4817 92 2312 437 51 2312 8951 57 2313 5243 41 2313 5391 25 2313 7641 94 2314 561 66 2314 3175 28 2314 992 4 2315 9812 0 2315 4285 57 2315 6780 16 2317 2707 19 2317 7681 24 2317 573 76 2318 9338 54 2318 2692 55 2318 7349 65 2318 9783 6 2318 1865 48 2318 7275 36 2319 8036 8 2320 1713 33 2320 7583 79 2321 1980 31 2321 8938 81 2321 627 13 2321 2132 28 2321 744 29 2321 5391 26 2321 62 23 2322 3692 97 2322 3090 16 2322 1223 84 2323 9027 8 2323 8060 24 2324 1512 19 2324 5720 26 2325 9929 0 2325 1731 92 2326 7130 83 2327 4400 80 2327 7712 100 2327 9439 69 2327 8015 35 2327 4435 39 2328 8063 88 2328 7564 15 2328 5318 89 2329 618 34 2329 6023 96 2330 5070 77 2330 7111 56 2330 8943 4 2331 7277 22 2331 8387 88 2331 1086 46 2333 1831 66 2334 6322 37 2335 8136 86 2336 8464 46 2336 3269 36 2336 3231 84 2336 9855 61 2337 4976 33 2337 2027 86 2339 4217 35 2340 1567 1 2340 2003 93 2341 1910 75 2341 6435 96 2341 540 12 2341 6023 88 2342 4905 19 2343 5827 57 2343 2512 26 2343 9499 54 2343 8241 20 2344 9882 86 2344 845 100 2345 3598 72 2346 8151 89 2347 2208 18 2347 8259 99 2348 3256 72 2348 4805 29 2349 4654 0 2349 969 75 2350 3853 97 2351 732 92 2352 1520 30 2352 8799 68 2352 240 4 2352 8042 68 2353 1781 10 2353 2668 86 2355 9109 25 2355 8149 79 2355 2053 55 2356 138 12 2356 7018 23 2358 1544 56 2360 6281 18 2360 8880 77 2360 8458 4 2362 7315 56 2364 7035 94 2364 6011 43 2364 2929 18 2364 6541 6 2365 9315 97 2365 3591 5 2365 3704 46 2365 6279 24 2366 4938 92 2367 1511 50 2367 9165 44 2367 3467 63 2368 5100 9 2369 3179 61 2369 802 81 2370 8235 42 2370 5337 59 2374 1732 53 2374 2537 25 2374 7099 5 2375 9745 50 2375 9099 9 2375 6562 76 2376 1901 0 2376 1063 94 2377 540 73 2377 9542 44 2378 4137 7 2378 8689 43 2379 2123 35 2379 8150 97 2380 8893 82 2381 6 89 2381 9594 67 2381 5634 29 2381 6192 66 2382 6518 28 2384 8725 92 2384 2415 58 2386 3848 46 2388 3810 46 2388 7163 74 2389 6877 47 2389 4892 43 2389 7838 76 2390 4042 78 2390 1268 59 2390 6279 15 2391 5428 99 2391 3468 4 2392 5064 77 2392 8489 99 2393 9752 51 2393 4999 92 2393 7451 3 2393 5343 63 2394 3254 76 2395 2548 8 2395 8147 92 2395 1713 83 2396 8998 44 2396 5023 85 2396 5474 89 2397 1040 48 2397 5111 99 2400 6070 86 2403 6208 47 2404 7584 51 2404 6973 79 2404 5153 83 2405 9676 29 2406 2092 91 2407 9743 97 2407 6823 7 2409 4031 30 2409 1129 74 2409 7182 10 2409 8983 34 2411 8196 12 2411 5594 45 2412 2030 73 2413 3499 77 2413 4528 40 2413 3698 74 2413 9463 100 2414 5654 13 2414 3073 25 2416 9612 80 2417 1508 16 2418 9041 6 2418 9121 24 2419 3006 42 2420 2847 100 2423 2661 80 2423 8284 96 2424 4095 1 2425 8170 77 2425 687 61 2425 2711 79 2425 9694 90 2427 9146 100 2427 1995 46 2427 947 39 2427 6520 87 2428 1854 12 2429 5304 91 2429 4056 55 2429 8812 72 2430 4258 25 2431 8982 80 2431 1344 98 2434 5415 23 2434 4577 19 2435 111 61 2436 3814 53 2437 6648 8 2437 3737 11 2438 6404 1 2438 4237 44 2439 6094 33 2439 9044 65 2440 6329 53 2440 6790 88 2441 8371 70 2442 7100 64 2443 981 42 2444 4330 18 2446 9596 61 2446 2292 18 2446 8386 27 2447 5431 0 2447 128 99 2447 2875 67 2448 5154 10 2449 5091 36 2449 7760 43 2449 193 69 2449 1121 28 2450 634 20 2450 7086 90 2451 6938 92 2451 6608 49 2451 7220 44 2451 3496 37 2452 8791 63 2452 3399 14 2453 751 45 2453 6543 83 2453 9351 5 2454 4388 62 2454 9076 45 2455 4751 49 2458 8844 61 2458 1502 76 2458 9588 87 2458 4729 40 2459 991 27 2460 7852 21 2463 8125 2 2464 7807 81 2466 4079 78 2467 7905 96 2468 4247 67 2468 2979 97 2468 7930 96 2469 8882 47 2471 859 23 2471 1635 15 2471 1966 56 2471 8995 20 2472 1361 31 2473 3277 24 2473 1715 40 2473 3650 95 2474 7882 69 2474 5938 94 2474 2206 35 2474 1331 28 2474 3469 17 2475 2180 79 2475 8198 47 2475 6458 10 2476 6171 3 2477 204 20 2478 9617 5 2478 7972 64 2479 670 37 2481 7529 52 2481 8577 70 2481 9484 83 2482 3696 9 2483 8025 65 2483 9762 97 2483 3836 65 2483 9020 90 2485 9781 86 2485 4482 20 2485 1145 88 2487 5101 3 2488 2084 1 2488 6773 43 2489 7223 100 2490 2695 37 2490 5103 23 2491 8077 16 2491 4890 84 2491 4561 80 2492 1314 54 2492 4442 82 2492 9178 2 2493 8260 45 2494 9836 0 2494 3199 22 2495 3450 9 2495 6242 22 2495 4310 99 2495 6432 63 2497 3069 91 2500 9788 4 2501 1328 20 2501 9175 17 2501 3216 56 2501 30 100 2502 4295 22 2502 4694 8 2503 4329 72 2503 2055 64 2503 4613 53 2504 4152 42 2505 1361 21 2505 8357 1 2506 6356 73 2506 3315 76 2508 7291 45 2509 1827 77 2509 6103 83 2510 840 64 2510 892 86 2510 1570 44 2510 2055 60 2510 2430 86 2510 9797 34 2512 9389 52 2514 2317 52 2514 219 7 2515 4498 54 2515 2596 12 2517 9746 57 2517 2507 92 2518 1856 20 2518 7019 58 2519 2153 4 2519 7655 96 2519 6432 60 2519 3695 66 2519 7272 32 2520 7838 69 2521 163 30 2522 4525 21 2522 2862 91 2522 4918 73 2522 8916 13 2523 6191 66 2523 602 56 2524 2987 76 2525 2893 29 2526 6365 94 2526 7080 94 2528 1645 53 2528 5427 94 2529 8393 69 2529 6450 57 2530 3135 42 2531 5393 4 2531 1138 20 2531 6136 86 2531 2119 83 2532 9878 53 2533 2139 81 2533 1928 57 2533 5901 51 2533 4640 87 2535 7159 88 2536 4779 58 2538 4902 95 2538 305 75 2539 1078 97 2542 7146 44 2542 4129 80 2542 6953 8 2542 9753 23 2543 2757 78 2545 1773 41 2545 7874 11 2545 3455 31 2545 7564 38 2546 1191 88 2546 5451 79 2546 6024 24 2546 1177 97 2546 6037 74 2547 75 98 2547 3370 22 2547 5532 62 2548 1468 96 2549 8546 87 2550 5040 6 2552 8735 27 2552 1781 60 2552 7148 78 2553 833 24 2553 4332 91 2554 6608 50 2554 839 56 2555 7507 63 2555 5952 40 2555 5707 14 2555 5549 41 2556 8881 13 2557 384 93 2560 2750 92 2561 196 3 2561 9141 98 2561 6143 28 2561 5461 21 2562 9480 58 2563 4741 26 2563 2338 56 2563 6098 78 2564 7343 20 2564 1991 52 2564 9506 29 2565 4165 42 2566 2593 24 2566 141 73 2566 707 77 2567 889 67 2567 3010 14 2567 5188 46 2567 1116 94 2567 2928 33 2568 5740 2 2568 3005 0 2569 9832 28 2569 6549 47 2570 2047 3 2571 504 60 2571 4360 14 2573 1928 65 2573 6683 21 2574 6409 18 2574 9086 84 2575 2334 33 2576 1965 16 2576 4595 86 2576 5401 83 2576 2253 49 2577 213 31 2577 74 35 2577 5104 17 2578 3004 49 2580 702 47 2580 481 91 2582 138 4 2583 2320 35 2583 8982 51 2583 4460 56 2583 2212 100 2583 3787 84 2584 4655 65 2584 7027 13 2584 1970 19 2585 47 51 2586 7368 57 2586 4230 68 2586 3391 52 2586 3474 42 2587 2934 49 2588 8507 60 2588 9774 52 2590 8243 69 2591 9580 99 2593 9664 24 2594 6285 63 2594 9343 63 2594 3778 79 2597 4898 73 2597 7698 90 2597 5263 10 2598 5032 99 2599 9062 82 2600 3708 33 2601 7328 79 2603 3794 16 2603 6169 6 2604 304 38 2605 429 78 2606 5799 73 2606 9862 75 2607 2081 50 2607 2879 4 2609 8268 93 2610 6550 89 2610 3018 51 2612 8491 73 2614 5531 51 2615 3192 94 2616 7482 38 2617 6037 87 2618 5949 22 2618 9185 43 2619 697 39 2619 2172 63 2620 7465 85 2621 1299 94 2621 8629 18 2622 4434 18 2622 3760 60 2623 2855 3 2624 3314 59 2624 1181 35 2625 9872 47 2625 4824 47 2626 8941 8 2627 5317 62 2627 5696 78 2627 713 75 2630 4222 43 2631 1350 14 2631 4377 68 2631 2595 37 2631 3460 36 2633 8754 38 2633 2454 88 2634 7945 25 2634 8817 33 2635 1774 65 2636 6794 51 2637 2761 22 2637 4916 94 2638 4991 21 2639 8671 53 2640 2028 69 2641 3046 29 2641 6144 1 2641 7110 79 2643 480 53 2643 3170 80 2643 2055 9 2643 6292 19 2644 5400 96 2645 742 70 2645 2744 39 2646 6980 49 2646 8085 97 2648 1477 35 2651 1017 38 2651 7011 70 2651 8511 96 2653 6047 44 2653 6410 92 2653 7640 46 2654 5665 30 2654 2941 75 2655 2260 62 2655 389 63 2656 8427 99 2656 9891 9 2656 4401 60 2657 6730 69 2657 2889 100 2658 7168 25 2659 1440 12 2659 6135 3 2659 6252 36 2662 7892 87 2663 6035 6 2664 7743 93 2664 7589 87 2664 7824 56 2666 2243 90 2668 6749 22 2668 1180 77 2668 9087 0 2669 6646 0 2670 1687 53 2671 311 45 2671 1002 35 2672 9617 18 2672 2851 0 2673 7253 28 2673 8643 23 2675 3715 8 2676 8047 38 2676 5946 99 2677 7000 63 2680 1150 37 2680 701 89 2681 9338 3 2682 6571 81 2682 687 48 2682 4323 56 2683 2023 67 2684 9308 5 2684 1055 61 2685 6947 59 2686 7755 79 2686 3052 93 2686 9538 26 2688 6897 32 2688 920 2 2688 4527 85 2688 9997 77 2688 2804 32 2689 4923 14 2689 7467 67 2691 6114 76 2693 2537 56 2694 8751 43 2694 3525 43 2695 1609 23 2698 5590 21 2698 5051 21 2698 1518 87 2699 895 13 2699 6604 56 2699 6985 31 2701 3075 11 2702 8849 48 2704 4591 27 2704 4022 9 2705 2697 64 2707 3629 77 2707 8016 81 2707 6908 31 2707 4405 19 2707 6801 46 2708 3517 65 2710 7169 75 2711 8723 10 2711 110 27 2712 2736 32 2713 7700 52 2713 2852 49 2715 7232 96 2715 2841 43 2716 7475 97 2716 7745 85 2716 4611 71 2717 2732 39 2717 140 39 2717 7854 100 2719 4475 84 2719 5035 90 2719 8813 69 2720 1181 62 2720 3725 20 2720 5183 50 2721 8334 16 2721 3288 54 2721 5015 27 2722 6073 25 2723 3116 94 2724 9037 69 2724 8391 87 2725 8306 94 2726 4936 31 2727 7893 60 2727 3469 96 2728 8031 27 2729 148 87 2729 7740 88 2729 4102 96 2730 3236 36 2731 4865 50 2732 1975 85 2733 9490 97 2733 9789 95 2735 9356 45 2735 6197 6 2737 7638 75 2737 9662 58 2737 5589 4 2741 3983 59 2742 6208 16 2743 3019 72 2745 6038 90 2745 4682 95 2745 7588 97 2748 4386 40 2749 5755 53 2749 4690 64 2750 9574 82 2750 3017 25 2750 3881 77 2750 8082 7 2751 3082 96 2751 7478 24 2751 9457 56 2751 492 84 2752 9278 61 2752 2512 42 2752 4553 2 2753 5000 73 2753 6442 29 2753 2055 11 2753 6772 80 2754 883 47 2755 3037 9 2756 3528 53 2756 8897 32 2756 6541 28 2757 638 30 2759 501 78 2759 5594 81 2760 6758 33 2760 1262 18 2762 915 17 2763 9366 53 2763 6481 15 2764 5169 78 2764 1789 48 2764 4977 87 2765 5507 42 2767 8518 92 2768 9717 71 2768 2406 78 2768 9017 27 2769 6707 6 2769 8283 68 2770 7661 26 2770 6368 85 2771 8375 86 2772 8415 71 2774 8114 21 2774 3441 66 2775 906 63 2775 562 99 2776 8621 88 2776 916 50 2777 3367 4 2778 8795 26 2778 4240 98 2778 6561 36 2779 1525 61 2779 6223 9 2779 5362 80 2780 8572 68 2782 5128 27 2782 3474 98 2783 1028 79 2783 1376 55 2784 1967 30 2785 2886 68 2785 2513 76 2786 9263 36 2786 29 74 2786 4765 100 2787 1678 94 2787 7338 75 2788 13 0 2790 3727 52 2791 9180 20 2792 2076 100 2792 1860 18 2792 5285 82 2793 1241 64 2795 5672 25 2796 1602 91 2796 4682 9 2796 2685 12 2797 4211 35 2797 1218 87 2797 2526 42 2798 1854 10 2798 4929 38 2800 7923 72 2800 1562 92 2801 8102 53 2802 5176 62 2803 1703 94 2803 2218 88 2803 9347 79 2804 9035 16 2804 9859 41 2805 7243 78 2806 2497 70 2806 7592 60 2807 9586 71 2807 2980 53 2808 8831 75 2809 6972 58 2810 9665 79 2810 9552 56 2812 9604 2 2812 3881 20 2812 3762 16 2812 2868 5 2812 33 49 2813 2553 65 2813 3385 33 2814 9490 7 2814 9804 29 2816 1517 30 2816 8148 19 2817 7224 54 2817 4530 24 2818 7804 88 2818 536 41 2818 4811 2 2818 8307 87 2819 1454 1 2819 8725 99 2819 5923 68 2820 9692 32 2820 1948 11 2821 2078 18 2822 6218 73 2822 6902 89 2823 7669 17 2825 8168 58 2825 5701 92 2826 7500 86 2826 7524 79 2826 9713 45 2827 7347 21 2828 2860 67 2829 3278 29 2829 3059 71 2829 3982 98 2831 5265 42 2832 974 2 2833 518 44 2833 4496 44 2833 6231 74 2834 2539 37 2835 9785 81 2835 8387 28 2837 5744 78 2837 3885 71 2837 3694 46 2839 8322 11 2840 992 68 2841 6171 93 2841 686 66 2842 8946 35 2842 4128 72 2843 9585 90 2845 9730 59 2847 857 28 2847 3712 60 2848 5168 51 2848 5589 73 2853 9791 72 2853 8967 23 2854 224 6 2854 8521 80 2854 3865 69 2854 1622 39 2854 4595 16 2854 9661 54 2854 7171 35 2855 8743 14 2855 8252 59 2855 6156 87 2856 5854 23 2856 6749 8 2858 6025 40 2858 895 98 2859 5216 40 2859 8149 8 2861 7603 83 2862 3512 99 2862 4156 51 2862 6397 90 2862 6021 97 2863 3049 94 2865 2191 87 2865 9999 67 2866 4938 58 2866 571 86 2866 4837 52 2867 7023 67 2867 9084 12 2867 6889 12 2868 8293 8 2868 7518 25 2869 8872 83 2870 7892 24 2871 4767 12 2871 2757 68 2871 4073 48 2871 2933 57 2875 4656 32 2875 272 27 2876 8766 47 2876 4866 48 2877 3231 92 2879 8168 91 2880 322 63 2880 460 73 2882 5365 14 2883 9693 11 2883 8464 99 2884 1845 85 2885 8762 70 2885 229 84 2886 7768 68 2887 406 35 2890 1741 84 2891 1593 59 2893 551 75 2894 7549 92 2895 2558 76 2897 7570 44 2897 5236 68 2898 3572 74 2899 7175 12 2899 2878 12 2901 2851 23 2901 2884 49 2903 4136 64 2904 4488 98 2904 6644 61 2905 191 93 2905 8406 69 2907 1146 100 2907 3451 99 2907 5209 49 2908 4432 48 2908 3620 87 2910 5112 26 2910 1381 81 2910 1322 94 2911 783 53 2912 8310 53 2914 4491 49 2914 4011 29 2914 2314 56 2916 9508 80 2917 9230 50 2917 4884 77 2918 5873 48 2919 8021 36 2919 6114 60 2919 6726 86 2919 6714 67 2920 4731 52 2921 6570 89 2923 2878 15 2923 7376 60 2924 6660 72 2926 7245 19 2926 8073 43 2929 2795 60 2929 6545 13 2929 1092 43 2931 3384 83 2932 3925 48 2932 9655 56 2934 1693 53 2935 1413 6 2937 2063 85 2938 3828 97 2939 8200 73 2941 5586 98 2942 5347 49 2942 6604 6 2942 5466 75 2942 7728 30 2945 1155 57 2946 1363 92 2946 5482 51 2947 9657 46 2948 2838 94 2949 8102 25 2951 427 17 2955 3947 51 2955 1067 75 2956 3169 23 2956 9470 23 2956 6671 74 2956 8179 3 2957 1825 53 2958 5472 9 2960 1602 4 2960 3585 75 2960 6036 87 2962 8109 6 2963 29 24 2963 688 57 2964 456 70 2964 699 32 2965 8432 9 2965 2524 9 2966 5910 23 2967 1151 80 2967 4382 99 2967 9239 56 2968 2813 19 2969 6031 33 2971 3812 54 2972 8658 22 2973 5524 28 2973 6626 23 2974 7113 71 2974 2932 43 2974 8466 73 2975 8709 12 2975 5273 39 2976 6552 14 2976 3979 19 2979 7620 94 2980 6791 91 2980 1506 22 2980 7480 94 2982 6158 2 2983 3228 99 2983 1723 83 2984 2962 45 2987 2307 78 2987 1670 31 2988 2939 99 2989 1147 49 2989 9067 71 2989 6202 90 2989 5681 94 2989 4922 70 2990 4456 76 2990 7364 52 2991 2207 77 2991 8606 99 2992 355 27 2992 9346 54 2994 9654 47 2994 393 39 2995 3911 34 2995 9163 85 2996 4550 78 2996 292 91 2997 8916 2 2997 5165 13 2998 1223 60 2999 7032 70 3002 2671 3 3002 7991 77 3002 7852 92 3003 4541 16 3003 4632 33 3003 3329 42 3006 787 7 3006 3565 30 3006 5451 21 3007 5638 95 3008 5516 55 3008 8457 77 3008 131 59 3009 8631 86 3009 3101 11 3009 2646 49 3010 2565 60 3011 8549 84 3011 601 54 3012 9189 50 3013 620 65 3014 9564 6 3015 6174 60 3015 7726 70 3015 7474 28 3015 2364 72 3016 5274 52 3017 9185 4 3017 9995 99 3017 2048 43 3017 4174 41 3017 8466 67 3018 9398 34 3018 1458 71 3019 2958 72 3019 7090 35 3020 7029 65 3021 7403 33 3021 5449 81 3021 5743 61 3021 3146 33 3021 4304 77 3021 8859 54 3022 3534 54 3023 1532 81 3025 6633 54 3029 3124 60 3031 700 84 3031 4206 35 3033 9206 5 3033 3293 62 3034 7000 72 3035 1089 34 3036 6459 19 3036 9783 46 3038 4656 7 3040 1278 23 3040 3538 84 3041 9245 16 3042 5243 33 3042 9947 33 3043 9447 60 3043 9491 2 3044 7977 49 3044 539 81 3044 6947 25 3044 6029 18 3045 1802 56 3045 5159 79 3046 8918 45 3046 2754 67 3046 6038 87 3047 3067 7 3047 957 57 3048 2948 28 3050 7053 27 3050 3376 8 3050 1826 79 3050 7933 97 3052 9986 18 3052 1385 62 3052 7710 20 3053 4888 92 3054 2419 16 3054 4412 21 3055 5333 47 3055 7023 78 3055 6494 77 3055 2696 55 3056 7360 81 3056 9133 55 3056 7306 53 3057 7644 10 3057 3530 14 3058 1870 49 3060 5097 20 3060 3716 86 3060 7507 37 3060 1199 55 3063 4243 27 3064 2727 40 3066 8175 45 3066 6186 58 3067 2946 80 3067 8049 36 3067 9873 65 3068 6072 66 3069 3255 100 3070 1724 10 3072 8293 43 3072 9012 24 3073 8540 47 3073 2747 34 3073 9155 74 3074 5329 82 3074 4467 42 3075 8416 18 3075 3106 4 3076 1613 85 3076 7108 74 3076 4038 11 3076 9269 48 3078 815 44 3080 4553 39 3080 4943 53 3081 9906 38 3081 1519 1 3081 3751 65 3081 4395 24 3082 2371 4 3083 6941 23 3083 7476 52 3084 775 69 3085 9904 52 3086 461 22 3086 4142 21 3086 4674 5 3087 5661 1 3087 189 65 3089 7417 8 3089 4141 100 3094 7958 87 3095 7611 91 3097 1518 15 3098 269 90 3098 2066 65 3099 3854 92 3099 5132 14 3099 6758 81 3100 5884 24 3100 6442 69 3100 3686 89 3101 9523 28 3101 532 97 3102 1577 40 3102 1794 66 3105 6456 18 3106 8797 54 3107 201 92 3108 5287 43 3109 5288 77 3109 271 70 3110 2402 19 3110 6167 66 3111 2309 0 3111 7956 77 3111 5367 4 3111 8145 29 3112 4508 99 3113 6831 55 3113 8186 2 3114 9255 100 3114 3787 35 3116 8537 14 3116 4203 2 3116 5884 74 3116 9838 64 3117 2115 41 3119 4884 52 3120 7309 76 3120 4106 65 3120 9900 92 3121 4401 23 3122 1770 14 3123 9533 17 3124 4615 77 3125 9999 13 3127 5147 45 3127 4759 93 3129 5173 71 3131 8004 31 3131 5507 40 3131 1893 28 3132 4119 62 3133 4855 52 3134 371 24 3135 6923 100 3135 3443 49 3136 1147 73 3137 2504 2 3137 3117 70 3137 1341 89 3137 3874 51 3138 3733 15 3138 3154 47 3138 4501 80 3138 2265 21 3139 33 0 3139 9984 61 3139 9440 90 3140 6705 7 3140 5611 19 3140 260 76 3140 5177 37 3141 3788 51 3141 9786 79 3142 8253 48 3142 8811 3 3143 8821 44 3144 7072 43 3144 2467 84 3145 1733 24 3146 9673 39 3146 5192 58 3146 8926 60 3147 8961 100 3147 2017 26 3147 7355 86 3148 6924 6 3148 6975 62 3150 9765 16 3150 9571 8 3152 697 63 3153 7635 7 3154 9209 60 3155 3322 9 3155 5198 90 3156 7463 92 3157 9315 72 3157 807 96 3157 512 91 3157 7436 40 3159 5163 34 3159 2354 68 3159 8606 98 3160 5598 78 3161 1556 34 3161 8215 53 3161 3935 44 3162 2700 34 3163 4213 94 3164 87 58 3164 4544 43 3164 7089 71 3164 2337 56 3165 5171 15 3165 4084 81 3165 3995 23 3165 7150 61 3166 6914 37 3167 4693 35 3167 8121 34 3167 7978 91 3168 6150 52 3168 7262 46 3170 6885 4 3170 8447 93 3171 5218 85 3172 5546 34 3173 1808 32 3173 7632 21 3173 7068 9 3174 7828 70 3174 9741 30 3178 8845 1 3179 870 65 3179 6252 81 3180 7915 81 3181 5589 91 3181 4648 69 3181 5849 68 3182 2628 75 3183 6240 53 3184 8964 1 3184 7046 61 3185 7117 72 3186 1416 51 3186 8756 35 3187 7379 54 3187 1591 30 3187 3148 39 3188 1520 82 3188 8509 65 3190 5142 64 3191 6227 81 3191 5818 82 3191 1142 55 3193 4940 79 3193 2509 30 3195 4025 63 3196 5344 68 3197 3332 55 3197 8985 67 3198 6917 63 3198 4259 90 3198 6933 40 3199 9983 98 3200 1835 22 3200 1055 29 3200 9292 14 3200 4948 47 3200 5684 13 3201 5032 20 3204 5543 82 3204 1159 21 3204 463 0 3204 9258 85 3207 7977 42 3208 4314 79 3210 2916 98 3212 1795 55 3212 9436 11 3212 6490 5 3213 6881 28 3214 6490 27 3215 2133 17 3215 8123 13 3216 6972 27 3218 6286 42 3218 1732 98 3219 9551 28 3219 752 4 3219 6469 50 3220 3339 78 3222 5637 13 3223 5330 86 3223 9994 4 3223 5992 98 3224 1721 64 3225 3278 82 3225 8760 85 3226 4849 23 3226 3333 29 3227 3396 6 3227 3990 51 3228 3947 100 3231 4874 43 3232 3017 13 3232 371 69 3233 2017 90 3233 3335 18 3235 9800 56 3235 7495 56 3238 9560 35 3238 801 79 3239 3099 54 3240 2983 47 3240 5105 20 3240 8125 88 3241 536 88 3243 6164 69 3243 8266 77 3243 973 44 3244 1958 49 3245 7670 86 3247 2854 30 3247 2702 15 3248 3581 19 3248 9704 71 3248 7118 79 3249 5365 56 3250 2683 69 3250 3975 12 3250 2474 26 3251 5286 18 3251 7652 26 3252 4661 63 3253 9812 25 3253 3680 60 3253 471 91 3253 3873 61 3253 1810 41 3254 9313 85 3255 5983 23 3256 6906 99 3256 7089 65 3257 3923 68 3258 974 53 3258 4321 4 3258 6846 85 3259 663 37 3259 2671 58 3259 6735 7 3260 8669 65 3260 3052 8 3262 4286 48 3262 3461 45 3263 8412 6 3264 1769 86 3265 2011 21 3266 9503 68 3267 8601 51 3267 765 54 3268 1964 10 3268 811 87 3268 6346 4 3269 714 60 3270 594 44 3270 3383 33 3271 3266 11 3272 7701 76 3272 5004 77 3273 7896 18 3273 5246 100 3273 2812 10 3274 4614 74 3275 3191 63 3275 2265 4 3277 3807 70 3278 6651 6 3278 2474 98 3279 8254 86 3279 6533 72 3279 4828 17 3281 6343 12 3281 9319 63 3281 1179 56 3282 8719 12 3282 8180 0 3283 1881 33 3283 751 63 3283 8151 22 3284 761 42 3285 2285 74 3285 6692 71 3287 5152 65 3288 9858 41 3289 1616 75 3289 315 80 3290 1322 43 3290 8394 50 3291 9079 7 3292 4550 67 3292 5157 81 3294 1612 13 3295 9932 54 3295 7991 32 3296 3585 34 3296 920 45 3297 7068 44 3297 2201 78 3298 382 69 3299 2029 1 3300 3378 52 3301 6358 29 3302 949 26 3302 5709 39 3302 3621 5 3303 5947 16 3303 1466 62 3303 5094 76 3303 8447 44 3304 8665 54 3304 2250 97 3305 5613 100 3305 6937 40 3306 980 97 3307 6072 46 3307 4715 74 3308 9383 59 3308 2399 40 3309 1962 94 3309 7955 2 3310 433 19 3310 9499 11 3310 319 55 3311 68 3 3311 5999 2 3311 3880 97 3315 8425 97 3316 6050 61 3316 5295 84 3317 456 50 3318 1436 29 3318 8265 64 3319 4842 42 3322 9383 97 3323 1642 97 3323 6642 76 3323 5135 73 3325 3999 67 3325 6404 17 3326 2302 31 3326 726 82 3326 1035 18 3326 5779 63 3326 3093 3 3326 7848 67 3327 6521 43 3328 485 44 3328 4146 55 3328 9320 73 3330 4262 23 3331 4227 81 3332 5520 35 3332 9262 94 3333 9584 3 3334 1905 56 3334 1783 8 3335 5929 69 3335 8860 12 3335 7463 71 3336 8309 86 3338 2074 37 3338 8666 20 3338 9017 25 3341 1308 50 3342 8245 57 3342 4700 60 3342 2827 80 3342 6697 20 3343 4769 49 3343 48 80 3343 1322 55 3343 7219 33 3344 5982 20 3344 7120 66 3344 4337 6 3344 9519 22 3345 6772 5 3345 7606 88 3346 3663 53 3346 3862 52 3347 4464 50 3347 2589 71 3347 3432 94 3348 3142 0 3350 2604 29 3350 726 67 3350 4576 21 3352 2971 71 3352 6364 0 3352 2521 32 3353 8904 5 3353 5347 32 3353 9720 29 3353 5625 46 3354 1480 88 3354 4763 49 3355 5496 26 3355 8124 49 3356 1065 32 3356 9400 64 3356 9726 29 3357 8644 20 3357 6952 32 3357 6401 12 3357 2594 0 3358 1420 86 3358 9834 36 3358 9020 11 3358 4641 55 3358 9180 47 3358 4357 80 3358 7456 8 3359 7836 44 3359 5581 13 3360 9357 54 3360 4707 32 3361 7994 99 3362 335 53 3362 1910 82 3363 1191 44 3363 9273 45 3364 9643 39 3364 6555 90 3365 9019 65 3365 6668 26 3366 1332 98 3366 878 35 3368 333 33 3368 5059 31 3368 3728 85 3370 6349 19 3370 6156 77 3371 5770 24 3374 2591 69 3375 8712 49 3376 5875 9 3378 2781 6 3379 9930 49 3380 7657 30 3381 8520 0 3382 3941 32 3382 5815 0 3384 5595 34 3384 984 40 3386 1769 63 3387 6034 56 3388 3938 72 3389 5520 92 3391 8691 95 3391 5106 36 3391 4802 13 3391 2322 3 3393 5590 86 3394 188 73 3394 4037 30 3395 4714 40 3395 6521 85 3397 8436 65 3398 2308 86 3398 1485 45 3398 4291 77 3399 1439 54 3399 6195 42 3400 5665 9 3401 7172 100 3401 5243 12 3402 3528 5 3402 1116 49 3403 6590 51 3403 8106 2 3404 8578 89 3404 905 30 3405 8795 9 3405 5988 97 3406 9588 26 3407 3436 19 3407 3605 84 3407 1386 35 3409 9959 86 3409 8289 71 3410 1186 15 3410 9146 27 3411 6561 71 3411 5459 86 3412 3069 89 3412 9756 3 3413 5661 81 3413 8693 58 3414 3065 28 3414 2252 50 3414 4249 59 3415 8859 73 3415 8346 50 3416 2622 51 3417 3369 62 3417 9090 34 3417 6587 43 3417 7816 11 3418 2803 78 3418 2394 100 3418 8259 72 3418 6612 93 3418 8628 23 3419 6041 31 3421 129 34 3421 1601 49 3422 5712 28 3423 8022 62 3423 7546 4 3423 881 75 3424 8598 5 3424 5094 19 3424 7190 13 3426 6985 78 3426 1114 55 3427 3262 83 3427 5518 50 3428 3792 70 3429 6614 10 3429 8627 64 3430 4608 12 3430 2804 47 3431 9896 70 3431 9917 37 3431 7821 80 3433 7417 53 3433 349 31 3434 935 35 3434 6486 19 3435 1229 8 3436 6003 74 3437 6688 73 3438 3273 12 3438 4115 20 3438 3032 11 3438 9653 75 3438 4318 99 3440 2019 7 3440 2131 16 3441 8232 80 3442 3139 67 3442 8657 42 3444 4645 73 3445 3158 72 3445 7501 36 3446 2127 86 3447 7276 62 3447 4785 43 3447 7225 34 3447 9753 41 3448 5677 71 3449 1609 63 3449 8245 46 3449 5109 20 3451 1157 4 3453 9065 42 3453 3072 75 3454 1245 51 3454 5726 65 3455 6630 60 3455 726 48 3456 4429 46 3456 5820 66 3457 5850 85 3457 5917 39 3457 6863 14 3458 4741 99 3459 9001 40 3459 9854 12 3460 1751 23 3462 5776 86 3463 4524 89 3463 555 63 3464 7461 77 3464 7707 96 3466 5832 7 3468 9432 19 3469 5989 21 3471 7427 37 3472 6951 24 3472 5778 86 3472 1126 98 3475 2346 42 3475 5220 86 3475 3127 93 3477 2212 15 3477 6908 61 3479 4705 51 3479 3684 90 3479 9249 21 3479 7789 84 3479 7660 18 3480 7768 73 3480 6892 45 3481 4090 60 3481 3620 2 3481 3130 0 3482 1413 59 3482 6838 29 3482 124 40 3483 6446 35 3483 8482 67 3486 2973 91 3486 736 75 3486 9207 63 3487 7762 42 3487 8926 24 3487 8018 55 3487 1491 11 3487 3678 72 3487 1954 31 3489 1526 40 3489 6767 69 3490 308 65 3490 4408 35 3492 1767 36 3492 4046 34 3493 6150 81 3495 4969 20 3496 7911 47 3496 9589 68 3496 5998 58 3497 9953 57 3498 5588 51 3499 1108 1 3499 5623 44 3500 6196 73 3500 2672 88 3501 500 75 3502 1543 26 3502 457 51 3502 2850 63 3503 3118 18 3505 4502 47 3505 4711 45 3505 8547 67 3506 2303 72 3506 187 59 3507 5155 72 3507 5006 98 3507 1003 71 3507 8195 75 3508 7425 95 3508 5612 44 3509 3522 70 3509 7148 7 3509 382 63 3510 1812 11 3511 4557 55 3511 6691 74 3512 7598 54 3513 4538 51 3514 8479 91 3515 5295 99 3515 3189 28 3515 8929 62 3516 5821 6 3516 169 14 3516 7364 75 3516 3562 58 3517 9210 71 3517 9113 17 3518 8526 28 3518 4197 34 3518 7883 48 3518 3263 6 3518 5552 25 3519 6748 60 3520 5931 97 3521 8700 86 3521 4354 34 3522 8168 85 3522 8436 40 3523 6276 18 3523 6802 9 3523 8728 66 3524 7346 83 3525 3572 68 3525 5515 72 3525 3302 97 3526 9631 81 3527 9969 20 3527 7639 57 3528 5090 81 3528 6384 30 3529 3532 6 3529 7836 0 3529 9002 82 3530 1817 13 3532 6665 15 3532 6841 81 3533 8606 80 3535 6914 64 3535 3694 3 3535 4071 23 3535 6657 88 3536 8243 69 3537 4311 4 3538 2573 64 3538 8150 10 3539 2211 62 3539 6582 16 3539 7278 76 3540 771 100 3541 427 30 3543 6939 14 3544 9824 47 3545 790 67 3545 3248 60 3545 6864 0 3547 1185 35 3547 7400 41 3548 7988 48 3550 8770 12 3550 6186 73 3551 9698 13 3552 4262 61 3553 3546 66 3553 6425 88 3553 989 3 3553 7708 14 3554 6504 45 3555 704 98 3556 5935 86 3557 2791 51 3557 2088 59 3557 933 20 3559 8237 95 3559 5219 31 3559 6660 0 3560 1108 63 3561 1061 79 3561 8224 63 3561 5078 43 3561 8663 88 3561 2549 36 3561 5071 7 3562 4996 79 3563 3880 92 3563 3305 32 3563 5413 66 3563 902 0 3564 2006 5 3565 4893 0 3566 1788 39 3566 5673 16 3567 4884 88 3567 7641 72 3568 234 97 3569 7139 45 3569 2233 81 3569 8791 1 3569 9313 29 3570 6023 79 3571 8341 65 3571 5318 93 3571 5371 99 3575 4676 6 3576 3425 57 3577 8936 50 3578 4967 47 3578 1217 87 3579 2458 77 3580 9690 72 3580 9221 47 3581 1784 84 3582 2548 68 3582 2043 44 3582 4039 33 3582 358 78 3584 1953 54 3584 6005 3 3585 8751 17 3585 7335 29 3585 9729 90 3586 7782 65 3586 1014 80 3587 2923 47 3587 3816 0 3588 4319 74 3589 2837 75 3589 5182 52 3590 4922 6 3590 3461 79 3590 9647 93 3591 8960 34 3592 8601 49 3592 4257 35 3593 6551 68 3595 8002 64 3595 2435 12 3595 78 32 3597 5942 34 3597 1431 30 3597 7871 66 3598 7401 10 3599 1767 18 3599 3380 39 3601 3615 74 3601 7329 86 3601 7840 44 3603 3996 57 3603 7118 5 3604 4944 65 3604 6972 88 3604 4196 84 3604 5037 48 3605 5939 100 3606 2048 91 3607 2651 41 3607 4077 43 3607 3057 54 3608 2049 18 3608 2740 56 3609 8833 12 3610 1354 14 3611 8001 47 3612 1146 73 3613 9022 48 3614 3905 83 3614 863 47 3614 2710 14 3615 3636 18 3617 8418 11 3617 4965 2 3618 1849 26 3618 8320 98 3619 878 91 3619 3541 77 3620 5616 41 3620 4128 65 3620 3717 1 3622 538 70 3622 1520 21 3623 1807 45 3624 4023 71 3624 5695 40 3624 9950 80 3624 8346 54 3625 6357 96 3626 1084 12 3626 5526 76 3630 4366 35 3630 562 66 3631 809 14 3631 707 97 3631 9379 99 3632 6549 56 3632 2099 85 3633 3299 54 3635 8043 9 3636 5253 50 3636 4010 10 3639 3340 44 3639 1738 65 3639 4645 4 3640 3143 37 3641 5680 62 3642 455 9 3642 5768 97 3642 3694 86 3643 4654 64 3643 8853 59 3643 9908 37 3644 6065 49 3645 5512 82 3645 3605 44 3647 5976 70 3647 1880 72 3647 7707 70 3648 9329 18 3648 3322 4 3648 7541 98 3649 1859 59 3649 970 88 3650 9843 66 3652 9006 40 3652 4040 27 3653 6681 75 3654 5734 8 3655 5471 38 3655 21 63 3656 4537 6 3657 2173 71 3658 8817 1 3658 6878 11 3658 5060 14 3658 9688 53 3660 1035 60 3661 9323 99 3662 5557 75 3663 102 97 3664 5410 11 3664 8212 69 3664 7094 82 3664 8169 97 3665 7225 93 3666 1593 64 3666 518 11 3666 5111 59 3667 6258 35 3667 2198 73 3667 391 58 3668 3202 37 3668 8579 4 3669 9137 89 3672 397 9 3673 5141 87 3673 6452 56 3673 1312 11 3673 7864 43 3674 350 87 3675 3736 66 3675 2892 2 3677 4076 1 3678 2082 84 3678 2678 87 3678 5949 47 3679 3427 93 3679 9945 50 3679 6394 23 3680 8316 7 3681 9307 32 3681 222 66 3681 8364 6 3682 225 88 3682 131 16 3682 7461 60 3682 8883 42 3683 2007 98 3683 861 63 3684 259 65 3684 2076 45 3686 3928 74 3687 4044 7 3687 1610 24 3689 3107 49 3689 3427 96 3689 6729 69 3690 2509 50 3690 1748 80 3690 8470 99 3691 1024 74 3692 821 22 3692 453 14 3692 5710 14 3693 124 17 3693 1883 84 3693 4712 46 3694 2860 86 3694 4356 58 3695 6062 38 3696 7450 47 3697 3509 22 3698 8399 0 3698 8737 79 3699 1595 58 3700 8418 13 3700 2884 54 3701 9180 29 3701 8080 17 3704 6923 21 3704 851 47 3704 3358 62 3706 6686 61 3706 3188 57 3707 776 3 3707 4189 97 3708 7997 6 3708 5701 87 3708 3459 84 3709 6322 0 3710 1393 92 3711 4188 40 3712 8514 14 3713 6766 24 3713 3899 55 3715 2442 82 3717 4438 82 3717 40 77 3718 6594 82 3718 4092 60 3718 1805 34 3720 9063 73 3720 3448 38 3721 7784 63 3721 6766 71 3721 9427 32 3722 3695 99 3723 1954 41 3723 2999 35 3723 5305 5 3723 777 78 3725 1550 8 3725 4825 91 3725 7297 25 3726 613 36 3726 5930 7 3726 5265 41 3726 1749 11 3726 3678 15 3726 4659 68 3727 2032 85 3727 5792 12 3728 5973 28 3729 222 87 3729 6572 63 3730 6489 98 3731 8948 43 3732 1197 29 3733 2636 52 3734 3347 68 3737 9516 1 3737 9078 22 3738 9525 21 3740 5061 7 3740 1444 45 3740 7353 12 3742 4440 73 3743 1827 40 3744 114 9 3745 5486 59 3745 2560 96 3746 1091 12 3748 3266 39 3749 9248 57 3749 6388 61 3752 895 25 3752 7393 9 3752 4774 96 3754 3217 81 3754 2021 81 3758 2993 56 3759 3663 87 3759 8036 6 3760 7242 34 3760 3793 38 3761 9796 51 3763 7881 72 3764 7093 62 3765 4021 92 3767 2989 25 3768 7705 36 3768 5816 85 3769 7707 36 3769 6867 40 3769 9210 33 3769 5771 6 3770 422 39 3771 4735 32 3772 8833 12 3772 2224 97 3773 4063 97 3773 748 0 3773 6191 29 3773 6230 4 3774 2908 66 3774 1169 66 3774 850 45 3775 7239 90 3775 4380 99 3775 6376 68 3776 264 12 3777 6506 63 3777 6387 55 3778 6764 24 3779 6947 67 3780 8659 24 3780 8098 15 3781 6840 54 3784 700 28 3784 5042 63 3784 1858 12 3785 5471 41 3786 3928 34 3788 2610 63 3789 1249 5 3790 4352 32 3790 6129 60 3791 7381 55 3794 3252 30 3795 7733 58 3796 3698 3 3796 7923 23 3797 9456 73 3798 4213 21 3798 4226 80 3798 247 21 3798 4328 29 3798 4282 3 3798 3567 53 3800 9515 66 3800 6974 58 3802 3965 14 3802 3574 61 3803 4508 52 3804 8563 75 3804 9438 63 3804 8583 9 3805 8011 48 3806 4701 13 3806 3568 23 3807 5434 15 3807 9430 49 3808 6620 0 3808 9210 52 3808 877 49 3810 7224 16 3810 1626 27 3811 755 97 3811 2898 74 3812 9648 69 3812 901 73 3813 247 35 3813 7873 54 3815 2801 98 3816 6535 87 3816 9407 73 3817 2125 51 3818 3066 67 3819 3375 91 3820 7634 79 3821 9522 88 3821 9941 47 3822 764 100 3822 9660 79 3822 5827 96 3823 1371 98 3823 534 74 3823 4938 67 3825 4611 52 3825 8065 69 3828 4822 50 3829 6116 69 3830 2182 59 3831 9859 16 3831 1054 35 3831 898 10 3831 4474 24 3832 1534 70 3833 2972 85 3833 73 91 3836 8304 22 3836 1722 92 3836 2549 24 3837 1394 28 3838 9153 81 3839 2251 71 3839 3683 51 3839 960 53 3841 9351 21 3842 3901 79 3842 2016 58 3842 3990 68 3843 4244 39 3843 1502 91 3843 3165 3 3843 3043 58 3844 5362 32 3845 5107 27 3846 3440 46 3846 2637 62 3846 8782 91 3847 8304 19 3847 1141 30 3847 7629 33 3847 1233 4 3848 8027 96 3848 889 88 3848 7565 3 3848 7372 28 3848 7952 22 3849 4495 23 3849 4070 40 3849 2167 50 3849 9817 69 3850 7332 64 3852 2098 83 3853 3688 19 3854 2200 73 3854 4287 23 3855 7799 47 3857 1975 0 3858 3592 23 3858 5039 18 3859 8904 39 3859 3030 77 3860 3947 73 3860 7772 82 3860 2150 15 3861 6574 53 3862 4785 50 3862 6454 84 3864 9291 25 3864 8214 12 3865 764 85 3865 5723 28 3865 5298 1 3865 7509 73 3869 1695 66 3869 181 16 3870 3096 87 3870 8108 19 3871 1375 45 3872 3380 59 3874 8479 68 3876 1057 30 3877 298 42 3877 1971 34 3877 2853 91 3879 4852 56 3880 9230 10 3880 6510 26 3880 6694 4 3881 7257 23 3882 5225 59 3883 3776 63 3883 9201 22 3884 5211 80 3884 5850 53 3884 6196 29 3885 959 3 3885 7023 63 3889 6651 51 3889 8379 47 3889 7517 9 3891 4264 69 3892 6227 88 3892 1577 95 3892 8654 10 3893 2186 61 3893 5700 74 3893 7992 76 3894 3423 47 3894 9394 34 3894 9775 40 3894 5156 83 3895 4388 93 3895 5081 9 3896 5331 91 3897 2039 20 3898 835 71 3899 664 57 3900 5949 89 3901 3372 71 3902 8792 97 3903 3995 78 3903 1987 18 3903 4201 27 3903 7424 6 3905 1832 50 3905 3036 10 3905 7562 14 3905 3389 74 3906 648 83 3908 5461 7 3910 7176 0 3910 5724 39 3910 7968 36 3911 8891 90 3911 3007 29 3912 1096 33 3912 6202 74 3912 6959 80 3912 7520 51 3912 2655 99 3913 3973 79 3913 2523 38 3914 3561 78 3915 2118 3 3917 60 21 3917 2251 30 3918 6118 18 3919 639 43 3920 2180 62 3921 689 95 3921 4223 77 3922 1633 15 3923 6267 26 3924 8276 62 3924 3270 5 3926 2193 47 3926 193 68 3926 7807 51 3926 1140 30 3926 5093 36 3930 4645 98 3931 812 3 3932 6551 41 3933 5480 72 3935 3501 45 3935 3723 23 3935 8324 24 3935 8266 49 3936 810 34 3938 4003 23 3938 3236 23 3940 1435 57 3940 2520 69 3940 1098 76 3940 790 17 3940 563 0 3941 8533 78 3942 2554 4 3942 1164 73 3943 6453 68 3943 8787 82 3944 4788 33 3944 1189 5 3945 4179 72 3945 7422 37 3946 8840 37 3946 4936 15 3947 6025 98 3947 2793 73 3950 3764 52 3950 1723 8 3950 4637 36 3952 5475 36 3954 5247 99 3954 2999 85 3954 2377 32 3954 1855 18 3955 6364 59 3956 3793 43 3957 898 94 3958 602 55 3958 4506 46 3958 1254 38 3959 9024 34 3959 8350 81 3959 4534 48 3961 8062 41 3961 2750 84 3962 2663 16 3963 7496 69 3963 968 2 3963 7598 7 3964 1677 35 3964 9994 52 3964 9103 82 3965 5299 64 3965 4391 96 3965 6225 79 3965 4959 42 3965 6063 97 3966 6076 20 3967 7261 18 3967 5552 33 3968 6540 8 3969 4181 60 3970 7952 12 3970 6449 91 3972 2245 10 3973 1397 25 3973 3593 30 3975 9254 67 3975 9375 96 3975 4719 80 3976 1264 74 3977 1867 28 3977 9136 23 3978 168 49 3978 3292 71 3980 1199 33 3980 3698 82 3981 2795 99 3981 6358 72 3982 9881 13 3982 6392 90 3982 7750 56 3984 9970 18 3984 9734 82 3985 7685 90 3985 4486 81 3985 5708 44 3985 4328 95 3986 6467 61 3987 6183 49 3987 4422 10 3988 1910 71 3989 7137 16 3989 3107 37 3990 9196 61 3990 6598 90 3992 9652 72 3995 5425 72 3996 473 92 3996 8114 43 3996 2990 7 3997 4484 77 3997 3480 59 3998 9938 64 3998 5411 28 3999 2157 19 3999 1415 23 4001 3868 92 4002 7896 64 4003 475 26 4004 7053 51 4005 8022 77 4005 1252 6 4005 4368 10 4006 423 10 4006 3411 70 4006 4890 27 4007 8871 74 4007 9088 43 4008 9418 3 4008 5385 62 4009 641 6 4009 1039 58 4009 9647 20 4010 50 36 4011 5989 24 4012 6905 100 4013 2806 72 4013 2116 54 4014 899 74 4015 9753 17 4016 2583 95 4017 3788 62 4017 9971 42 4018 4064 68 4018 3784 78 4018 340 77 4020 3125 47 4020 3307 30 4021 8797 85 4021 9505 27 4021 364 25 4022 5813 6 4024 9305 46 4025 9825 9 4026 774 40 4026 3247 32 4026 2133 0 4026 1730 35 4027 8009 5 4027 454 63 4028 4589 74 4029 3293 49 4029 2674 60 4029 1150 34 4029 9115 66 4029 3213 13 4030 6455 77 4031 7331 92 4031 5104 87 4031 4015 2 4032 1891 59 4033 9 11 4033 9310 15 4034 7770 60 4034 557 96 4034 3856 95 4037 2072 67 4038 7149 82 4038 7354 63 4038 4714 87 4039 7492 1 4039 6337 65 4039 8845 41 4039 3900 55 4040 2031 58 4042 7737 42 4042 4350 44 4043 8038 3 4044 2607 74 4045 4110 50 4045 8638 17 4045 8571 31 4045 9140 54 4049 9658 44 4049 4638 29 4051 9102 64 4053 3262 38 4053 1350 55 4055 401 77 4055 8149 74 4056 9160 34 4056 6519 47 4057 6799 44 4057 2762 69 4058 3212 4 4058 1053 75 4059 3442 99 4059 6584 5 4060 8604 63 4060 9671 55 4062 5851 10 4062 7832 65 4063 890 5 4064 80 38 4065 7419 29 4066 989 44 4066 4024 18 4068 5249 89 4068 2142 11 4068 226 19 4068 6401 89 4069 2729 95 4070 3725 96 4070 3007 63 4071 5147 35 4073 5327 60 4073 7125 25 4074 4290 6 4074 2297 72 4075 4673 61 4075 8158 94 4075 940 46 4077 2642 92 4077 2090 18 4078 1269 92 4079 8556 59 4079 7137 53 4079 1095 36 4080 7641 51 4083 5133 0 4083 5096 21 4084 2833 60 4085 2359 39 4085 6559 50 4085 7355 59 4087 9345 91 4087 3498 31 4088 904 8 4088 7068 65 4088 9962 61 4089 7283 4 4090 1326 3 4091 8830 55 4091 4611 100 4093 7105 21 4094 1376 46 4095 2336 35 4095 2533 25 4095 671 46 4096 2189 60 4096 901 31 4096 3123 13 4096 8663 26 4097 6408 87 4098 7756 39 4098 812 86 4100 2848 100 4100 4643 70 4102 2464 52 4104 6689 41 4104 9210 28 4104 6039 9 4105 6025 98 4106 8636 88 4106 8372 98 4107 4358 16 4107 7307 60 4109 6994 89 4110 5414 93 4111 5140 23 4111 6825 99 4111 3000 22 4112 1133 52 4114 6870 54 4114 9211 93 4116 4955 67 4116 6623 89 4116 3351 11 4116 2148 95 4117 6873 2 4121 4110 58 4122 570 53 4122 7456 3 4122 7249 2 4123 7219 55 4123 8272 42 4123 371 3 4124 8822 82 4125 6281 71 4127 8673 22 4127 9897 29 4128 7064 39 4129 1397 0 4129 6212 94 4130 6791 57 4131 872 7 4131 738 72 4132 2743 16 4132 6003 3 4132 5787 38 4134 8257 76 4134 3046 50 4135 7864 64 4136 5399 27 4136 6268 69 4137 4593 98 4137 5063 53 4138 7102 11 4139 4863 79 4139 2265 84 4140 7180 2 4140 8904 91 4140 8358 84 4141 5240 83 4141 2562 73 4141 6797 89 4141 5686 9 4142 8321 76 4143 5103 20 4144 1293 67 4144 833 27 4145 8652 72 4145 7931 55 4146 9684 18 4146 2700 43 4147 6313 69 4147 1521 3 4147 1375 98 4147 6417 81 4148 7600 36 4149 9008 39 4149 1075 27 4150 3171 85 4151 2828 40 4151 8741 24 4152 5517 83 4152 5446 62 4153 6121 29 4155 5067 60 4156 5384 21 4156 3493 73 4157 3894 7 4157 9215 27 4157 124 93 4157 7299 82 4158 6016 91 4160 9869 63 4161 1914 71 4163 4633 36 4163 3244 84 4164 2877 60 4164 5626 56 4164 4771 45 4165 4679 55 4165 6573 27 4165 7959 51 4166 1271 66 4169 7318 50 4169 7039 74 4169 8924 1 4170 6742 0 4170 2382 96 4171 9145 15 4171 7557 19 4172 3777 49 4172 700 88 4173 2259 22 4177 7192 86 4178 1312 8 4178 8946 77 4178 8089 5 4180 5527 91 4181 8293 7 4182 2053 12 4183 7074 40 4184 9559 10 4184 520 36 4187 8070 81 4187 6928 46 4188 6982 63 4189 5286 36 4190 3617 53 4194 8608 7 4194 5058 73 4195 2289 18 4196 5610 84 4196 3542 66 4196 8918 92 4197 6050 17 4197 1323 37 4198 3343 42 4199 936 61 4199 9978 21 4199 1696 33 4200 3814 25 4201 7029 3 4202 8266 16 4202 2947 36 4203 808 12 4203 7400 64 4203 6188 64 4205 8595 11 4205 9677 55 4205 4065 44 4205 5146 64 4206 7015 60 4206 9662 29 4206 7480 13 4207 8803 49 4207 4608 14 4209 154 36 4210 8990 10 4210 4678 16 4210 6718 50 4212 6100 39 4215 4843 81 4216 569 83 4216 2071 11 4217 541 12 4218 3796 43 4218 3531 25 4218 4045 25 4218 6092 62 4219 3617 67 4220 5440 4 4221 9041 0 4221 6639 28 4221 4650 6 4223 9904 97 4223 7383 51 4224 6273 21 4224 3409 83 4225 1387 70 4225 5306 76 4225 4585 70 4225 5471 48 4227 4421 82 4227 503 24 4229 4458 56 4229 9974 12 4230 3618 34 4231 6386 22 4231 9564 6 4231 8544 29 4232 1643 68 4232 8987 4 4232 5629 71 4232 331 11 4232 5266 72 4232 4790 97 4233 1550 98 4233 9983 82 4234 4641 32 4235 3548 81 4236 2789 42 4236 9901 24 4238 1763 80 4238 1732 28 4239 9730 72 4240 8992 39 4241 2171 33 4241 1040 50 4242 333 11 4242 2463 59 4243 6074 35 4243 580 97 4243 8716 81 4244 8075 19 4244 7513 74 4245 718 46 4245 9184 77 4245 4370 69 4246 4569 94 4246 3293 52 4247 2430 26 4247 6699 90 4248 3266 64 4251 5780 78 4252 4215 60 4252 7545 95 4253 4732 10 4254 2031 36 4254 8420 19 4256 135 54 4258 4659 68 4258 6150 81 4258 6451 67 4259 9243 39 4259 9693 64 4260 763 36 4261 5789 21 4261 2043 18 4263 981 93 4264 6528 28 4265 9411 78 4265 2641 74 4265 8026 45 4266 8637 93 4267 6230 30 4268 4751 15 4270 2819 24 4270 8300 63 4271 2344 74 4271 2109 25 4272 2882 64 4273 4104 59 4273 8924 39 4275 9911 6 4275 4974 54 4275 5752 31 4276 9493 27 4277 5974 78 4277 1107 66 4277 575 64 4277 2737 14 4277 4371 93 4278 4063 68 4278 6546 14 4278 7389 60 4278 5984 66 4279 6071 0 4279 1629 33 4280 4992 27 4281 9516 97 4281 9188 75 4282 8152 46 4283 9241 21 4283 1334 21 4284 9176 2 4284 8116 62 4285 832 83 4285 919 55 4286 6891 42 4288 7273 31 4288 5542 76 4289 3760 27 4289 6537 72 4289 2109 94 4290 1732 3 4290 5809 0 4291 234 92 4293 4208 84 4293 3563 48 4293 6595 43 4293 6252 41 4293 6368 93 4294 11 73 4294 5299 28 4294 7950 34 4294 6548 19 4295 4768 81 4295 6420 43 4295 1102 58 4296 1673 72 4297 5319 83 4298 8209 82 4301 4690 77 4302 5286 63 4302 2603 61 4303 8479 66 4303 2434 98 4304 8704 54 4304 3392 57 4306 8563 28 4306 3128 21 4306 2830 55 4309 8502 92 4309 693 94 4310 7836 44 4310 2516 16 4310 554 19 4311 9425 63 4312 6939 90 4313 3674 17 4313 9216 99 4313 9291 39 4314 3770 23 4314 2960 16 4315 8802 52 4315 3533 55 4315 50 37 4316 4937 79 4316 6571 71 4316 7745 100 4316 8867 54 4316 1367 32 4317 8419 25 4317 8529 3 4317 2972 83 4317 7494 0 4317 1082 31 4317 9277 40 4318 4355 39 4318 2992 87 4318 8572 19 4319 6653 2 4319 646 96 4319 8269 63 4320 9347 72 4320 8816 25 4320 974 76 4321 2848 39 4321 5941 52 4321 5275 90 4322 342 45 4322 5502 94 4323 3316 8 4324 2852 55 4324 6372 45 4325 9649 50 4325 1500 95 4326 9705 16 4329 1973 7 4329 625 90 4329 9527 0 4330 8964 33 4330 7645 53 4332 2087 59 4332 5225 55 4332 2619 57 4332 4445 70 4335 4196 80 4335 2902 52 4335 1664 58 4336 935 100 4338 4518 24 4338 7992 69 4338 5779 15 4340 6174 57 4340 8890 60 4340 8893 37 4340 7699 81 4340 5881 16 4340 9233 51 4340 8451 41 4341 1692 29 4341 9516 57 4342 301 57 4342 4373 19 4342 8256 34 4343 374 55 4343 5193 87 4343 89 69 4343 7745 53 4344 9334 87 4345 4229 9 4345 1664 63 4345 9475 44 4346 518 40 4347 7888 3 4349 3341 1 4351 3364 90 4352 945 51 4354 701 99 4355 2544 25 4356 8539 77 4357 6426 61 4359 4262 47 4360 6150 31 4361 71 48 4361 7714 87 4362 6047 93 4362 4966 47 4363 1767 72 4364 7762 100 4364 5499 73 4365 2009 4 4365 4423 77 4366 3551 34 4366 7017 4 4366 6715 91 4368 6661 2 4369 8779 65 4369 8665 100 4370 637 65 4370 8220 17 4370 2783 70 4371 8348 51 4371 1652 0 4371 4511 89 4372 5373 35 4373 3372 11 4373 1582 77 4374 4344 15 4374 1496 49 4375 562 8 4377 434 60 4378 5962 93 4378 3822 99 4379 992 88 4379 4688 63 4379 4463 14 4379 4101 29 4380 644 56 4381 1669 43 4381 7073 50 4382 5253 37 4382 2375 10 4383 7571 93 4385 2633 26 4386 7681 85 4386 4060 30 4387 2422 40 4387 6950 33 4388 7759 89 4388 1925 14 4389 9677 22 4391 5339 5 4391 6593 78 4395 815 3 4396 6711 18 4397 5583 1 4398 285 58 4401 6800 68 4401 8973 99 4402 8020 17 4402 3647 52 4402 1737 7 4403 5542 12 4403 6345 54 4404 633 40 4404 1427 100 4405 8596 40 4406 4830 95 4406 8851 39 4409 6132 69 4410 5175 43 4410 3839 38 4411 7473 39 4411 9171 73 4412 3601 62 4412 9816 65 4414 8680 69 4414 5251 25 4414 5464 10 4414 5742 89 4415 9574 81 4416 9944 36 4417 7037 21 4417 2091 57 4418 5038 40 4419 1014 63 4420 6468 23 4420 8994 64 4420 6556 12 4420 1057 62 4420 5340 19 4421 829 13 4421 3047 54 4422 934 37 4422 7928 27 4422 1501 80 4424 6945 69 4425 925 89 4425 5741 0 4425 2382 9 4426 7678 56 4426 8483 63 4426 3836 44 4427 5845 40 4428 7908 37 4429 6902 28 4429 6555 57 4430 3763 67 4431 3162 1 4431 8383 73 4432 4846 19 4434 9375 87 4435 961 75 4435 7776 90 4437 8506 64 4439 1832 43 4440 2876 30 4440 7657 12 4440 9234 9 4441 4275 22 4442 2176 70 4442 7766 44 4442 8211 45 4442 8174 58 4443 3099 60 4443 9556 68 4444 620 44 4444 658 11 4445 9410 94 4445 7985 95 4446 2222 35 4447 3371 18 4448 5764 58 4451 7735 20 4451 3757 43 4452 5401 19 4452 1027 91 4452 1214 30 4453 8566 7 4453 5713 56 4453 6470 99 4454 5952 73 4455 4057 51 4455 5613 81 4456 4461 64 4456 8249 97 4456 5164 48 4457 9314 49 4457 5604 51 4457 50 67 4458 9619 9 4458 9416 56 4459 9378 3 4461 9130 39 4462 2466 47 4462 316 41 4463 5259 92 4463 2866 5 4463 6894 70 4463 9554 43 4465 3166 63 4465 5704 31 4465 9322 29 4466 3603 88 4467 3243 72 4467 4686 70 4467 5363 13 4468 9823 69 4468 8834 33 4468 5929 11 4469 6618 32 4471 5426 69 4471 6411 48 4472 1767 86 4472 2902 96 4473 4645 9 4473 9688 10 4473 8131 33 4473 7617 11 4474 3030 60 4474 6677 81 4476 421 48 4477 747 39 4477 2356 16 4477 7451 0 4478 3256 2 4479 491 37 4480 6918 87 4480 318 86 4481 5863 79 4481 8497 42 4481 7461 72 4482 5719 82 4482 4790 95 4484 5965 82 4485 8604 86 4485 393 61 4485 7688 50 4486 1855 81 4486 7093 11 4486 5971 16 4487 7169 49 4487 4119 70 4487 1104 40 4489 3707 23 4489 4101 27 4489 2910 85 4489 1130 90 4490 7466 32 4491 7313 80 4491 6577 57 4492 2015 1 4493 661 41 4493 3755 16 4493 3986 48 4494 2759 71 4494 648 20 4495 5651 51 4495 5521 22 4495 3761 9 4496 4080 19 4497 705 54 4497 1192 100 4497 2889 11 4498 5559 24 4498 2689 73 4499 6742 49 4500 357 48 4500 7457 25 4501 9078 26 4501 4576 90 4501 565 6 4502 3932 85 4503 1668 37 4503 9608 38 4504 5144 25 4504 8021 39 4504 5730 29 4505 6603 69 4505 3519 43 4506 8278 43 4506 2475 81 4507 3251 7 4507 5937 38 4507 8542 99 4508 8380 84 4509 4748 17 4509 972 31 4509 2227 27 4510 3600 23 4510 1797 96 4513 9618 76 4513 5487 76 4513 4622 0 4513 8178 90 4514 4993 100 4514 6904 32 4514 8146 25 4514 8403 39 4516 4715 3 4516 5016 19 4518 1367 57 4518 7725 16 4519 4637 21 4519 507 59 4520 6514 92 4520 9919 1 4520 2065 81 4521 1218 37 4521 4800 33 4521 5961 50 4522 5815 3 4522 1548 15 4523 5661 52 4523 9196 44 4523 3058 93 4524 2668 31 4524 3885 43 4524 1651 11 4525 7464 42 4526 5732 42 4526 26 79 4527 7046 87 4527 2805 98 4527 397 21 4528 4024 47 4528 1265 32 4529 2228 86 4529 2067 46 4530 8355 4 4531 1343 9 4531 9814 30 4532 4314 57 4532 2513 78 4532 2549 76 4532 68 69 4533 6487 75 4535 7618 51 4536 8653 44 4537 6723 19 4540 8076 75 4540 1581 50 4541 3780 71 4541 1315 9 4542 6206 12 4544 5997 76 4544 4862 23 4545 4842 21 4546 560 67 4546 1752 93 4547 5217 6 4547 7041 6 4549 9108 6 4550 30 29 4551 5119 64 4551 4984 9 4552 6325 70 4552 6990 26 4552 1025 35 4552 9125 91 4553 8511 21 4553 7863 25 4553 6715 1 4556 905 28 4557 7314 49 4557 6334 55 4557 1987 52 4557 9694 79 4558 9708 83 4558 551 13 4559 1967 15 4559 3985 48 4560 2461 21 4563 2633 76 4564 6258 12 4564 673 70 4564 4578 39 4565 8217 6 4565 12 58 4566 8222 69 4567 8521 33 4567 7196 52 4568 3027 94 4569 8831 89 4569 6638 79 4572 4591 99 4572 72 56 4572 8892 45 4573 3561 55 4573 299 75 4573 141 16 4573 927 58 4573 7379 87 4573 2086 13 4574 8318 39 4575 9858 33 4577 9953 64 4577 8104 100 4580 2883 94 4580 711 33 4581 49 56 4582 4174 4 4582 5349 100 4583 780 93 4583 5259 52 4584 2020 78 4586 3817 18 4587 3050 90 4587 4761 94 4588 6855 18 4588 242 74 4588 1265 66 4588 9219 62 4588 1084 22 4590 8493 89 4591 1058 28 4592 2059 67 4593 5555 87 4594 1737 25 4594 2135 60 4594 5007 10 4595 8689 22 4595 9751 41 4595 9125 93 4595 2117 13 4595 76 55 4596 9712 53 4596 6794 30 4597 8087 96 4598 5982 5 4598 1530 26 4598 8694 24 4598 5440 50 4599 6580 44 4601 1864 5 4601 3633 74 4602 8795 46 4602 3816 98 4603 3893 52 4604 1451 1 4605 6616 26 4605 2178 49 4605 6298 1 4606 8552 6 4607 6386 87 4608 9370 59 4609 4144 35 4609 5462 44 4610 1565 61 4610 1606 70 4611 7939 46 4611 202 52 4612 325 51 4612 3224 45 4614 4278 17 4614 9468 85 4615 7115 72 4616 2471 12 4616 2480 81 4616 1131 66 4617 4180 82 4617 3651 19 4617 6508 57 4618 5337 19 4618 3007 100 4619 5559 70 4619 1964 27 4619 9202 12 4620 2048 47 4620 8473 42 4621 2406 70 4622 2387 72 4623 5654 72 4623 4346 86 4624 2417 85 4624 5202 15 4625 3667 93 4625 7820 21 4625 2615 54 4625 9964 48 4625 5804 60 4626 2203 6 4628 7507 26 4628 6655 14 4628 9151 87 4629 4593 3 4629 692 2 4630 9033 64 4632 9716 14 4633 9677 54 4634 1574 63 4635 682 2 4636 9304 8 4637 1591 28 4639 8369 48 4639 518 11 4641 7101 22 4641 6245 40 4641 6870 97 4641 4720 12 4642 7523 20 4642 9734 96 4644 2020 13 4644 4257 61 4645 8007 67 4645 1997 100 4646 646 98 4647 9123 90 4647 608 91 4648 9398 76 4648 5407 85 4648 8298 37 4649 5273 65 4649 1088 93 4651 2037 48 4651 4451 75 4652 1084 79 4652 8946 30 4654 2624 38 4654 5144 43 4654 5767 46 4654 8483 64 4655 1227 26 4655 6767 100 4655 7918 60 4655 2717 17 4657 164 99 4658 3393 31 4659 4844 33 4662 5703 36 4662 7332 32 4662 1229 41 4663 8034 10 4663 542 28 4663 9666 67 4664 7671 47 4664 3116 37 4664 6792 36 4665 3369 26 4667 4864 68 4667 3431 77 4667 1956 8 4668 4632 45 4668 7104 54 4669 5456 57 4669 1679 1 4669 9790 22 4670 7554 33 4670 2270 50 4670 9623 0 4670 9801 29 4671 111 72 4671 2873 75 4674 1158 0 4675 3224 9 4675 5953 89 4676 7435 54 4676 6299 10 4678 5742 100 4678 1502 95 4678 2824 7 4680 4427 32 4681 7565 33 4681 5487 57 4682 7152 66 4682 5321 70 4683 7387 39 4683 9530 44 4683 2329 91 4685 1130 87 4685 2930 28 4686 1864 16 4687 43 32 4687 2002 94 4688 7584 48 4689 5526 20 4689 8820 13 4691 9355 47 4692 6658 100 4693 6142 38 4694 5154 60 4694 5448 45 4694 8734 76 4694 6544 26 4694 6663 35 4695 5480 54 4695 5989 21 4696 8881 26 4697 9938 99 4698 2707 7 4698 4232 73 4699 9384 94 4699 9380 71 4700 7982 28 4700 5482 36 4701 5372 24 4702 9565 17 4703 2909 32 4703 6742 71 4704 3596 90 4704 3195 17 4704 7114 91 4705 7321 25 4708 8409 90 4709 5959 32 4710 1513 43 4711 581 72 4712 6679 52 4713 7460 61 4715 9885 65 4715 7905 23 4716 3006 86 4716 1841 90 4716 2709 0 4716 2418 21 4717 4752 42 4717 7974 54 4718 3888 54 4718 9819 67 4721 799 0 4722 4735 8 4722 7952 73 4723 7786 41 4724 7812 30 4725 2610 29 4725 6829 17 4725 4999 41 4727 65 62 4727 201 41 4727 5559 56 4727 746 87 4728 9179 31 4728 1481 97 4728 9358 89 4729 607 14 4730 7458 22 4731 6207 33 4731 8310 68 4733 215 99 4733 7802 62 4734 7811 26 4734 3542 43 4734 5153 3 4735 6442 85 4737 9402 95 4737 390 3 4738 5373 9 4739 3863 88 4739 2381 25 4739 2034 2 4740 6157 59 4740 7232 69 4741 7139 11 4741 2963 56 4742 7261 17 4742 5649 96 4742 7367 79 4742 376 45 4743 6675 87 4743 9353 49 4745 2389 88 4746 9104 69 4746 6902 21 4749 3212 14 4751 3276 17 4751 1109 53 4752 2110 68 4752 634 25 4754 5294 73 4755 3486 71 4755 7099 30 4756 1929 88 4756 6381 6 4757 9727 8 4758 1168 57 4758 5667 11 4759 2321 61 4759 6617 61 4759 1482 61 4761 5191 83 4761 967 66 4762 6126 65 4762 6105 90 4762 9208 22 4762 2167 100 4763 7854 27 4763 2978 36 4764 7142 14 4764 2664 10 4764 5713 71 4764 4465 82 4764 8483 68 4765 4291 5 4767 3559 47 4767 8289 80 4767 5301 8 4770 1587 59 4770 6944 44 4770 2242 55 4771 4184 60 4771 1226 61 4772 2367 36 4772 1586 75 4772 9671 52 4772 1197 29 4773 3167 96 4774 8772 42 4774 1513 56 4774 6590 72 4775 5241 12 4775 9741 100 4775 8635 16 4775 4116 15 4775 2906 26 4775 4209 52 4777 37 63 4779 111 45 4781 8378 27 4782 6593 66 4783 9139 100 4783 7900 79 4784 5194 62 4785 5959 33 4786 7379 38 4787 7756 9 4788 577 84 4788 4632 91 4788 9520 37 4789 2145 70 4789 3398 52 4789 7619 10 4790 6837 8 4792 3792 88 4792 912 48 4793 4394 21 4793 9202 45 4794 5137 75 4794 4318 86 4795 8930 20 4795 6829 99 4795 2150 33 4796 8169 91 4797 1498 92 4797 7640 66 4798 5062 92 4798 2454 38 4800 9344 23 4800 1474 54 4802 8571 79 4803 6828 59 4804 1660 1 4805 1617 5 4806 8387 53 4807 4311 37 4808 6865 78 4808 9865 85 4809 3461 71 4809 4466 11 4810 1428 58 4810 2653 18 4812 6882 16 4812 9985 79 4813 4794 59 4814 9085 92 4814 8 19 4814 4877 22 4815 476 73 4815 160 25 4815 8421 92 4816 9544 34 4817 5689 78 4817 7879 26 4818 9809 64 4820 4857 35 4820 9629 38 4822 9134 35 4823 6312 68 4823 1412 24 4824 6542 36 4826 2585 89 4827 5848 48 4827 4346 34 4828 7880 49 4830 662 71 4832 5707 34 4833 7409 89 4835 2162 55 4836 4485 41 4837 9566 78 4837 7325 53 4837 8717 17 4838 629 86 4839 7528 68 4839 7828 60 4840 1329 37 4840 905 8 4841 1300 13 4841 287 58 4841 4514 21 4842 1070 22 4842 270 67 4842 5632 19 4843 6794 92 4844 5152 68 4847 9924 73 4848 5964 46 4848 1227 65 4848 7436 40 4849 9916 27 4849 8252 21 4849 6943 5 4851 8180 10 4852 766 21 4852 7408 40 4854 8611 72 4854 4028 82 4856 9515 66 4858 7673 83 4858 3422 100 4858 3525 43 4859 7873 80 4860 6660 38 4860 5966 59 4863 1831 19 4863 4107 31 4864 5090 8 4864 1868 21 4865 1838 45 4865 313 14 4866 9641 96 4866 8715 24 4866 2164 26 4867 8632 68 4867 5733 69 4868 4248 40 4871 6468 30 4871 2639 92 4872 3743 50 4873 6060 60 4873 3116 76 4873 8577 53 4873 3536 65 4874 9531 61 4875 6416 95 4875 3811 59 4877 5669 20 4878 1174 96 4879 7076 8 4880 9948 53 4880 6910 20 4881 1502 71 4882 2550 48 4883 7240 93 4885 3128 10 4887 1166 4 4889 1997 0 4889 228 42 4890 3129 13 4891 8218 22 4891 8908 90 4893 5533 36 4893 4734 90 4894 7024 57 4894 3849 89 4895 7335 33 4896 9505 13 4896 4987 23 4897 1864 53 4898 9825 70 4898 5296 99 4899 2611 71 4899 7205 13 4900 3141 54 4900 51 85 4902 640 90 4902 4481 75 4902 2299 61 4904 4111 94 4905 3771 48 4905 8116 56 4905 3112 5 4905 959 78 4905 3395 41 4905 3472 50 4906 5505 26 4907 8539 34 4908 4463 64 4911 3938 100 4911 9639 82 4911 7133 70 4912 321 97 4912 1133 30 4912 2412 47 4912 3959 0 4913 4057 79 4914 1015 25 4914 104 29 4915 9885 11 4916 9603 95 4916 4585 45 4917 1392 22 4917 2808 43 4918 5839 38 4919 2394 52 4920 7550 62 4920 2875 2 4921 5475 47 4923 4536 98 4923 6268 87 4924 3378 52 4924 1339 47 4924 8025 52 4925 632 1 4926 3245 23 4926 3448 92 4927 8722 60 4927 936 52 4927 2689 43 4928 2545 51 4928 5570 35 4929 4866 34 4930 352 20 4930 6176 66 4932 8963 43 4935 80 21 4936 5135 46 4936 2182 89 4937 1906 10 4938 2625 93 4942 7620 90 4942 6376 100 4943 5445 63 4944 4856 88 4945 9203 20 4945 2945 4 4946 7836 87 4946 8127 57 4946 2042 56 4947 626 32 4947 7328 10 4947 9462 29 4947 8260 38 4947 236 12 4948 5625 5 4948 5106 49 4949 7748 77 4949 4797 4 4950 7119 80 4950 3771 6 4951 5880 26 4951 2849 10 4952 3497 42 4953 1292 1 4953 7248 23 4954 2351 95 4954 1291 57 4955 8604 63 4957 5781 93 4957 2728 89 4959 252 36 4959 5320 24 4960 2988 35 4960 926 3 4961 3151 55 4962 1670 96 4962 7015 1 4963 9413 96 4965 4068 86 4968 1665 92 4968 422 36 4969 6603 28 4970 4759 31 4970 5607 83 4971 7317 22 4971 1143 44 4974 748 5 4975 6229 53 4976 198 10 4976 6252 97 4976 4961 10 4977 9666 90 4978 4232 45 4978 4465 92 4978 7515 65 4979 8609 95 4979 3486 74 4979 3596 40 4980 1181 33 4980 1567 72 4981 9710 10 4981 1552 99 4981 9501 53 4982 2021 84 4982 6991 57 4983 2968 14 4984 2436 67 4985 7853 12 4986 1037 54 4986 4265 87 4988 5078 29 4990 7246 15 4991 6968 44 4991 1028 69 4992 2628 91 4993 5974 90 4993 9500 47 4995 5886 1 4995 6662 40 4995 2340 15 4996 4794 95 4996 75 86 4997 757 34 4999 2240 54 4999 3110 5 5000 4898 63 5001 7908 66 5001 4979 85 5001 843 1 5002 4591 73 5003 3382 41 5003 4724 68 5003 7237 8 5004 8250 50 5005 4670 1 5005 9185 1 5007 7219 29 5008 4888 77 5008 1722 23 5008 7315 80 5009 5990 30 5010 316 12 5010 4401 65 5012 4055 36 5012 1451 49 5013 1070 83 5015 6167 65 5016 8657 99 5016 4072 60 5017 8116 36 5019 7294 100 5019 4117 27 5019 9456 18 5020 5270 5 5020 15 0 5020 314 46 5021 5778 7 5021 2567 38 5021 6924 43 5022 4408 40 5022 1342 41 5027 5341 41 5028 7207 20 5029 3803 93 5031 2886 91 5031 1258 92 5031 2839 91 5032 5018 26 5032 4179 88 5032 6465 84 5032 5682 77 5034 9145 4 5035 6357 53 5035 2989 96 5037 1664 43 5037 2151 36 5037 2567 73 5038 6656 77 5039 7297 91 5039 1792 50 5040 8729 33 5040 8783 32 5042 1737 25 5043 2494 81 5043 5605 14 5046 6566 100 5046 3415 41 5047 5516 2 5049 2864 66 5049 6918 98 5050 9557 86 5050 1690 88 5050 6674 49 5051 1224 95 5051 8044 10 5051 3218 14 5052 4970 89 5053 2375 8 5053 3226 20 5055 8327 50 5056 9172 8 5057 5026 96 5057 8985 28 5057 7963 94 5058 175 75 5059 2666 8 5061 2180 72 5062 7642 81 5063 7451 96 5065 9119 99 5066 2452 81 5066 6746 94 5068 2715 89 5068 9260 92 5068 800 70 5068 9173 93 5070 9286 81 5071 8106 50 5072 6581 28 5072 3347 74 5072 4718 8 5073 7031 84 5074 8608 30 5074 987 10 5077 2415 11 5078 2047 52 5078 2445 97 5079 5884 74 5079 3063 99 5079 8488 23 5082 2921 96 5082 6402 13 5083 2591 41 5084 4159 67 5084 5955 59 5084 4357 51 5086 5238 23 5086 5049 63 5087 449 32 5087 5588 21 5087 4616 100 5090 1603 46 5091 6553 47 5091 1564 65 5092 4506 12 5092 9392 28 5092 8452 36 5093 4750 76 5093 8596 62 5093 3163 32 5094 8052 84 5094 2683 35 5094 3767 3 5095 3589 82 5095 5684 80 5095 4278 46 5095 6951 57 5096 3983 48 5097 5328 12 5097 7011 8 5097 662 11 5098 364 41 5098 4046 29 5099 9409 38 5100 6810 65 5101 2227 100 5101 9305 18 5103 9115 77 5103 3146 20 5105 9009 30 5106 3899 15 5106 557 93 5107 7088 93 5108 1257 60 5108 6027 80 5111 9951 48 5112 1228 51 5113 8817 71 5114 7091 1 5114 1898 17 5115 4802 10 5115 8684 68 5118 5855 67 5118 4595 62 5120 245 45 5121 1061 11 5124 2680 1 5124 5769 73 5124 2641 63 5125 3342 74 5126 4843 53 5126 5876 30 5126 1228 65 5126 308 52 5127 729 42 5128 9099 58 5129 3374 24 5130 6700 74 5131 4161 59 5131 827 47 5131 5025 70 5132 9891 92 5132 2640 89 5136 6876 66 5137 7446 89 5138 9891 37 5139 2200 6 5139 3137 56 5140 4051 93 5141 3192 34 5141 8748 30 5141 9034 63 5142 9321 60 5143 7981 92 5145 6879 39 5146 432 63 5146 9866 9 5148 9571 76 5148 2112 31 5148 511 56 5149 9451 69 5149 6986 70 5149 7278 59 5150 9755 4 5150 5985 87 5150 3422 94 5151 7325 72 5151 8235 87 5152 7324 11 5152 9635 67 5153 370 23 5153 6561 68 5154 1752 85 5154 2228 92 5155 1173 19 5155 2414 82 5155 1552 24 5158 9279 36 5158 4090 51 5158 8432 42 5158 9683 100 5158 5859 31 5160 2685 84 5160 4331 24 5160 6073 12 5160 2027 86 5162 5962 21 5162 4632 53 5162 403 47 5162 9895 84 5163 1337 17 5163 3030 16 5163 2461 50 5163 1486 79 5164 6029 64 5164 3759 13 5165 1771 94 5165 5406 48 5166 8331 65 5166 4494 66 5167 2124 8 5168 9636 88 5169 4585 73 5170 4290 87 5171 1587 77 5172 8049 81 5173 13 10 5173 3469 72 5173 5505 79 5175 1334 89 5175 917 23 5177 7218 20 5178 534 90 5178 2668 67 5180 8178 66 5182 1233 8 5182 2950 60 5183 8131 37 5184 4546 0 5186 3675 23 5186 8895 84 5187 4137 32 5187 6981 80 5187 758 84 5189 9861 3 5189 8870 22 5190 8305 16 5190 2852 79 5193 3001 55 5193 8898 79 5193 9191 80 5194 4330 83 5194 1038 12 5195 5371 89 5196 7591 34 5196 5892 44 5197 2217 68 5197 3252 70 5198 4123 56 5198 4658 89 5199 3968 26 5200 6119 95 5200 3753 47 5200 4782 52 5201 1218 30 5203 7922 19 5203 4324 24 5203 4298 94 5203 7767 32 5204 1822 28 5204 2051 7 5205 4225 98 5206 6103 17 5207 1340 55 5207 894 12 5207 3121 57 5208 7606 91 5208 6036 26 5208 4592 48 5208 1277 15 5210 8164 10 5210 5765 93 5212 2510 37 5214 4789 76 5215 7724 91 5216 4784 63 5216 4023 30 5217 314 48 5217 2788 42 5217 8183 67 5217 7980 66 5219 6909 41 5219 5759 31 5220 0 38 5221 6855 75 5224 1727 0 5225 4546 81 5226 3183 88 5226 7881 13 5226 9200 20 5228 6975 67 5229 8756 75 5231 214 18 5232 9535 11 5234 3517 55 5235 1755 29 5236 2931 43 5236 4253 68 5236 129 50 5236 5745 100 5236 6505 20 5238 5931 48 5238 399 55 5239 394 76 5239 8958 75 5239 3467 73 5239 7947 19 5240 4448 27 5240 413 58 5241 2527 83 5241 1232 61 5242 5409 5 5243 9731 26 5243 7194 64 5243 3891 88 5244 9751 14 5244 5567 77 5246 903 21 5246 5814 96 5246 9998 48 5247 3103 88 5247 7780 100 5248 8802 19 5248 5164 32 5249 1909 45 5249 8760 15 5249 4840 92 5251 6144 41 5253 7101 30 5253 2449 81 5253 5536 99 5254 9020 75 5255 4273 42 5256 772 50 5256 2355 81 5257 7524 97 5257 4480 36 5257 3749 15 5259 7863 41 5260 924 66 5260 1751 41 5261 8786 27 5261 3793 60 5261 6835 17 5261 6913 1 5261 3140 73 5261 5013 97 5263 5526 24 5265 8118 23 5265 1812 11 5266 5793 20 5266 4234 57 5266 9197 77 5268 6735 7 5269 6077 84 5270 5509 40 5270 3118 66 5270 3499 29 5270 9311 56 5270 5240 49 5271 5119 39 5271 4985 62 5272 898 37 5273 965 89 5274 4205 100 5275 7410 46 5275 416 49 5276 7248 48 5277 4627 13 5279 9823 86 5279 4895 76 5280 5047 80 5281 4105 56 5283 9679 89 5284 8582 32 5286 7661 26 5286 7519 16 5287 3154 98 5288 2917 56 5291 9208 8 5292 4847 84 5292 2980 26 5293 6667 37 5293 7704 82 5295 8946 19 5296 5889 25 5296 1775 35 5297 1989 1 5299 9297 97 5299 6282 37 5300 8091 26 5300 2206 82 5301 1721 77 5302 4723 72 5302 5092 14 5303 532 64 5304 7867 45 5304 8773 96 5305 9880 69 5306 6450 42 5306 6210 65 5306 8549 27 5309 707 65 5309 7670 0 5309 5575 88 5310 8312 28 5311 1806 97 5311 536 78 5311 9217 74 5311 2755 61 5312 9388 53 5312 7463 4 5312 3945 51 5313 5674 75 5314 2663 51 5314 107 44 5314 2172 87 5315 8643 87 5316 411 69 5317 9721 91 5317 4898 60 5317 5323 44 5318 7179 85 5318 2226 51 5320 1854 53 5320 3812 66 5320 5083 87 5321 3110 48 5321 8640 50 5321 5987 1 5322 8675 70 5323 1191 68 5323 3829 30 5324 7148 42 5324 5935 25 5324 4531 95 5326 5526 16 5327 5281 16 5327 8167 42 5327 2527 17 5328 5555 82 5328 516 25 5329 9048 77 5329 4392 8 5330 4662 69 5330 3744 63 5330 8919 33 5332 934 42 5333 6399 86 5334 5653 6 5334 1075 22 5335 2362 3 5337 9230 25 5337 4986 64 5337 1703 62 5338 8961 93 5338 6628 38 5340 8908 65 5342 9169 33 5342 1429 13 5343 1441 52 5343 7170 36 5344 5720 68 5344 299 85 5346 8942 71 5347 1057 0 5347 9588 14 5348 3945 8 5348 1124 46 5351 5805 16 5353 1700 81 5353 1122 51 5355 9597 25 5356 4860 99 5358 7453 69 5358 6634 79 5359 6696 51 5360 3286 84 5360 7156 54 5360 3036 66 5361 3451 93 5361 1815 87 5362 471 95 5363 7309 66 5364 7363 22 5364 1496 99 5364 4702 37 5366 6208 55 5366 4289 96 5368 8361 38 5368 4516 36 5368 3064 45 5369 4520 95 5369 5325 17 5370 4580 68 5370 6357 11 5371 3721 20 5372 7146 30 5374 6971 51 5375 3753 18 5375 2925 16 5375 7275 25 5378 3659 29 5379 3706 81 5379 7257 20 5380 3181 45 5381 6750 63 5382 1756 5 5382 2578 14 5382 1807 90 5382 109 91 5383 439 27 5384 5604 34 5384 3319 82 5384 9905 75 5385 9887 68 5385 8116 11 5386 114 16 5387 3979 35 5387 9637 66 5390 830 18 5390 1003 49 5391 780 80 5391 7857 20 5391 3501 45 5391 4933 70 5393 3171 63 5393 5758 76 5394 6059 16 5395 2345 23 5395 2613 74 5396 6639 59 5397 4323 11 5397 3867 85 5397 5889 77 5398 7907 49 5398 4014 13 5398 1784 11 5401 168 35 5401 8474 90 5401 7581 49 5403 6564 56 5404 2779 63 5406 4673 47 5406 7106 87 5407 3333 15 5410 301 68 5412 3010 79 5413 2545 78 5417 5007 73 5418 9752 37 5419 946 53 5420 768 91 5421 3014 75 5422 9548 31 5423 6082 30 5424 1200 40 5424 1950 10 5424 8269 53 5425 8774 30 5425 3568 77 5426 4701 47 5426 7200 49 5427 2428 62 5427 5289 96 5427 6333 78 5428 4005 77 5429 5922 72 5430 666 83 5431 5901 1 5434 6890 91 5436 4420 7 5437 8083 13 5437 9454 25 5437 3079 32 5437 2578 80 5438 993 54 5438 9395 35 5438 4832 92 5438 614 26 5439 5234 93 5439 3405 78 5440 5588 19 5440 2663 91 5440 7851 14 5441 1120 82 5441 9349 96 5442 7313 79 5442 3421 54 5442 5842 16 5442 4081 6 5443 1600 93 5444 1341 33 5446 2282 61 5448 3741 64 5450 6802 45 5451 773 42 5454 7612 91 5454 5271 20 5456 894 94 5457 1231 94 5457 9679 79 5458 7313 12 5459 2369 51 5459 9148 99 5460 8928 79 5461 185 44 5464 7714 84 5464 5757 24 5466 5896 25 5466 7307 78 5466 9375 65 5467 9508 53 5468 283 1 5468 5656 74 5468 3491 71 5469 8233 27 5469 7884 69 5470 1064 49 5470 7853 28 5471 6241 82 5471 2088 51 5472 4622 91 5473 2593 89 5473 7778 85 5475 2592 85 5476 2061 94 5476 4470 93 5477 6981 3 5477 7386 82 5478 1917 48 5478 721 93 5482 5037 55 5482 7793 94 5483 5645 19 5485 2561 56 5485 6067 15 5486 4797 26 5486 9128 78 5486 6281 32 5486 5661 61 5487 9610 33 5489 7800 97 5490 1064 99 5491 1189 1 5491 1350 99 5494 6962 44 5495 4909 30 5496 2279 15 5496 6837 29 5496 9164 66 5496 3756 63 5497 9607 59 5498 4615 12 5499 5083 98 5499 931 90 5500 6178 33 5501 9259 69 5502 814 85 5503 1884 64 5504 8286 37 5504 8411 43 5504 9613 29 5507 7578 65 5508 3735 67 5509 3390 25 5509 4836 71 5514 9992 80 5514 5467 38 5518 6958 63 5519 6467 78 5519 665 29 5519 3516 71 5519 8900 14 5520 208 26 5520 3204 65 5520 6742 97 5520 1463 19 5520 4829 3 5523 6401 14 5523 6331 66 5524 4966 53 5526 6622 20 5526 7591 45 5527 1520 94 5528 1381 84 5529 2763 22 5529 8053 45 5530 226 76 5530 2884 26 5532 515 12 5533 9545 81 5535 2734 43 5536 5175 92 5536 9040 81 5537 4154 31 5537 7945 61 5538 5675 8 5538 7336 0 5539 1509 90 5539 1385 6 5539 4773 13 5539 2376 24 5540 2742 17 5542 6483 7 5543 1093 89 5544 2917 74 5544 3306 5 5545 6912 15 5545 9207 71 5545 3522 22 5546 135 73 5546 6350 98 5546 7447 7 5548 6851 9 5548 9575 60 5549 8717 8 5549 1471 6 5549 5111 99 5549 9693 17 5549 9237 53 5550 8258 18 5551 1398 45 5552 5317 89 5552 7498 71 5552 8679 19 5553 3699 78 5553 3387 58 5553 6605 10 5554 9039 96 5556 2122 19 5558 9568 98 5558 8832 49 5561 82 52 5561 3313 2 5562 4245 68 5562 3304 39 5564 1049 12 5564 6799 42 5566 9350 47 5567 8906 5 5567 1251 10 5567 9214 18 5568 3644 51 5569 6220 94 5569 10 0 5570 4211 0 5570 5536 80 5570 7003 3 5572 3300 53 5572 7599 92 5573 9586 73 5573 8191 93 5573 6069 59 5574 6504 91 5578 1364 51 5579 9195 47 5579 7519 6 5581 1459 79 5581 2174 45 5581 4303 77 5581 2176 70 5582 1127 8 5582 2030 93 5582 4949 74 5583 2425 44 5583 5202 54 5584 9024 95 5585 9675 47 5586 6060 90 5586 5546 9 5586 0 100 5587 1823 46 5587 2471 25 5587 845 22 5588 7846 53 5588 2922 9 5588 9131 74 5588 374 13 5591 6760 5 5592 8642 44 5592 9032 49 5593 8345 33 5593 4371 97 5593 3016 14 5593 7779 22 5594 762 20 5595 4561 73 5596 2511 50 5597 7003 66 5597 5956 62 5597 3475 13 5598 6857 66 5598 6471 78 5599 1910 36 5600 9349 15 5601 7494 69 5601 6525 80 5603 7056 76 5603 2174 36 5603 602 57 5605 8643 93 5606 4378 63 5606 4271 81 5606 6113 34 5607 5680 59 5609 8293 31 5610 3450 88 5611 6055 18 5612 1390 82 5613 4450 54 5613 4387 48 5613 1834 90 5613 9973 54 5614 5282 46 5614 2519 75 5615 3696 37 5615 516 46 5616 2425 45 5617 1464 39 5618 9511 74 5620 1513 30 5622 7629 76 5622 3819 3 5623 5170 3 5623 7806 67 5623 1480 65 5623 7209 88 5624 7708 70 5625 9357 88 5625 5189 84 5626 9490 85 5626 2622 55 5626 2135 100 5627 9236 34 5627 4163 86 5628 9767 11 5628 3361 30 5629 1242 45 5630 586 85 5630 6745 36 5630 3378 31 5632 1443 22 5632 7947 94 5632 4461 62 5632 8156 23 5633 2563 59 5633 8656 60 5635 8121 86 5636 6031 24 5636 8148 86 5636 5180 20 5636 4661 81 5637 1339 67 5638 1921 54 5638 6629 36 5640 4653 79 5640 1051 23 5642 5024 26 5642 1774 38 5643 4779 39 5645 7997 40 5645 7831 61 5645 6086 73 5647 82 0 5647 258 71 5648 9946 35 5648 2771 27 5648 6447 51 5649 4464 40 5649 6055 14 5650 2743 58 5650 7623 48 5651 2900 6 5651 7703 60 5651 6504 12 5652 6209 95 5652 6560 66 5652 8015 64 5652 9863 87 5653 7436 28 5654 142 55 5654 3577 78 5654 1425 25 5654 7929 53 5655 8727 92 5656 234 63 5656 4007 91 5657 4077 56 5658 9775 32 5658 5975 75 5659 8136 3 5659 5961 69 5661 2661 26 5662 4325 4 5663 603 74 5663 7558 39 5663 5145 36 5664 311 45 5664 2019 31 5665 2169 43 5665 9194 3 5665 1744 43 5665 1389 33 5666 4573 53 5666 5601 29 5667 8991 98 5668 7004 58 5668 1422 87 5668 523 3 5669 9845 0 5670 8274 13 5670 5795 83 5670 4751 79 5671 3669 89 5671 8802 30 5671 5603 100 5671 1764 44 5671 2822 42 5673 6358 48 5673 2303 45 5673 1948 86 5674 4090 21 5674 7032 34 5675 299 37 5677 5369 3 5677 452 33 5678 6137 79 5679 6382 53 5682 9073 95 5682 6742 87 5682 2799 85 5683 9926 73 5684 6268 52 5685 77 77 5685 2866 92 5686 1546 25 5687 3561 0 5687 7862 80 5688 9257 31 5688 5341 77 5689 997 33 5689 7434 70 5690 8217 31 5692 9057 25 5692 1141 71 5692 8139 19 5693 527 96 5693 5909 66 5694 5766 51 5695 2192 87 5695 9734 12 5696 9012 3 5696 3245 83 5697 5812 51 5698 9736 15 5699 8375 38 5699 2407 30 5701 2147 13 5701 7296 36 5701 9246 10 5702 5160 71 5703 5843 62 5703 452 96 5704 8418 69 5704 8506 29 5705 5358 35 5705 6076 71 5707 9013 13 5707 7766 14 5707 3955 99 5708 803 5 5708 9577 29 5709 1322 35 5711 6158 3 5712 9374 59 5712 1324 43 5712 3790 29 5713 4113 36 5714 9360 57 5714 3766 46 5715 9578 9 5715 6428 53 5716 2463 3 5716 6170 88 5717 8199 73 5718 1639 70 5720 7029 62 5721 2245 58 5721 4411 5 5723 8037 48 5724 7176 13 5724 2689 100 5725 1412 70 5727 807 7 5727 627 23 5728 7361 27 5730 6254 63 5730 9431 35 5730 1136 80 5731 1302 89 5731 5919 1 5732 1785 69 5732 3096 39 5733 3253 8 5733 3937 45 5734 3996 93 5736 7072 53 5738 3635 100 5738 6064 38 5739 5523 65 5739 6961 48 5740 7999 43 5741 562 57 5741 4701 4 5741 4921 89 5742 2992 79 5742 166 27 5743 2179 26 5743 8396 45 5744 5220 77 5745 3042 41 5745 6890 54 5746 3282 48 5747 20 55 5749 4549 100 5749 3476 27 5749 7072 38 5749 2880 0 5750 4066 99 5750 4309 57 5752 9216 99 5753 2367 34 5753 6523 85 5754 6389 92 5754 4062 90 5755 5497 66 5755 2814 83 5757 2881 59 5758 8832 65 5758 5093 37 5758 8774 35 5760 6644 87 5765 8081 65 5766 1126 4 5768 6911 14 5769 2849 54 5769 3372 51 5771 5860 5 5771 9319 66 5772 7369 35 5772 8784 89 5772 2689 43 5773 9245 76 5773 2346 67 5773 4452 12 5773 5559 95 5774 8927 28 5775 1690 54 5775 6351 90 5775 592 39 5775 6002 56 5775 8154 86 5775 574 15 5776 1761 32 5777 512 70 5777 3895 16 5778 955 51 5779 6034 25 5779 6243 68 5780 9283 26 5780 7440 38 5783 9208 39 5783 2261 71 5784 3917 88 5784 8790 52 5784 8212 95 5785 4616 4 5786 3445 2 5786 9290 99 5787 7499 70 5788 9324 2 5789 228 35 5789 4774 60 5791 2011 6 5791 723 36 5792 3968 13 5794 9619 68 5795 9098 91 5796 9296 78 5797 7102 1 5797 1841 82 5799 8727 47 5801 8817 19 5801 209 93 5802 7879 33 5803 6796 32 5803 9216 100 5803 2 7 5804 9777 73 5806 3744 19 5806 6942 40 5807 690 75 5809 7763 36 5809 2245 5 5809 849 73 5810 1033 24 5810 1371 33 5810 7653 19 5810 1330 4 5811 7341 99 5813 9662 96 5814 6378 44 5815 535 3 5815 3592 48 5816 8679 63 5816 1197 12 5817 2741 45 5818 1032 78 5819 4263 6 5819 7712 2 5819 5929 39 5819 7650 51 5820 7888 94 5820 1718 24 5821 7699 17 5821 434 8 5822 204 25 5822 4850 62 5823 2039 28 5823 8257 65 5824 9691 0 5824 6377 57 5825 3413 43 5827 7496 73 5827 8120 52 5827 4972 33 5829 1942 87 5829 7830 78 5831 2271 63 5834 6858 19 5835 1131 100 5835 2100 73 5835 1664 3 5837 7258 23 5838 5040 24 5838 1625 93 5838 9890 26 5839 5009 19 5840 299 44 5840 9748 31 5841 6765 18 5841 2918 7 5841 1453 57 5841 8415 79 5842 897 91 5843 268 27 5843 2074 31 5844 1950 50 5844 2123 17 5845 543 62 5847 7572 29 5847 37 72 5847 7567 86 5847 7944 63 5849 5424 46 5849 1319 21 5849 1950 33 5850 9645 49 5850 6013 17 5850 8297 47 5850 1075 20 5851 6403 8 5851 9243 68 5851 3936 47 5851 3703 65 5851 3146 21 5851 4922 32 5852 3906 25 5853 7678 10 5854 2060 91 5854 4016 33 5854 5494 75 5855 9938 84 5855 1025 7 5855 8766 33 5856 4156 86 5857 5977 12 5857 6351 18 5858 5535 98 5859 9485 64 5861 955 5 5861 9964 0 5861 6983 6 5862 1805 4 5862 2041 90 5863 5899 46 5863 3016 98 5864 2584 26 5866 8990 91 5867 9499 26 5868 3200 56 5869 4369 21 5869 5848 99 5870 219 43 5870 2500 72 5870 1278 43 5871 7057 48 5871 4001 18 5872 2415 67 5873 6323 43 5873 3035 67 5873 6439 54 5874 1851 53 5874 8050 91 5875 7687 24 5875 4111 89 5875 3011 83 5875 8361 24 5876 1010 44 5877 7292 58 5881 2888 37 5882 1883 52 5882 1871 55 5883 6576 74 5884 292 63 5886 7686 65 5887 6113 77 5888 1991 51 5889 3865 26 5889 1728 6 5890 5688 42 5890 9962 19 5891 8104 21 5891 4685 47 5891 8753 13 5892 4241 65 5893 1113 22 5893 650 87 5893 3954 62 5893 3373 32 5895 1702 38 5896 1268 90 5896 4930 62 5898 4988 91 5898 6847 13 5900 4037 5 5901 4127 27 5902 4904 19 5903 6466 82 5904 3130 76 5905 1475 40 5906 3191 14 5906 7046 24 5906 1431 67 5906 5539 69 5907 5121 16 5907 4145 60 5908 4848 42 5908 6382 28 5909 7116 61 5909 902 3 5911 1161 18 5912 1056 69 5913 9159 95 5913 6592 26 5913 7388 61 5914 957 49 5914 4901 95 5915 9024 8 5915 8038 30 5915 1980 72 5916 4606 89 5916 474 53 5917 3354 55 5918 50 0 5919 6822 90 5919 9486 37 5919 760 97 5920 2658 35 5921 6409 91 5922 7838 79 5923 7168 30 5923 9217 69 5923 2475 66 5924 3812 78 5925 9838 41 5925 4414 0 5926 3627 24 5926 3448 2 5930 4609 17 5931 2325 77 5932 2825 78 5933 9037 98 5934 8477 60 5934 2914 48 5934 1729 4 5935 4432 87 5935 5323 50 5935 938 25 5937 7094 59 5939 8880 60 5939 8803 91 5939 8247 69 5940 3525 78 5940 5890 66 5940 9843 46 5941 7199 64 5941 7634 8 5942 8764 51 5942 1640 76 5942 6935 13 5942 7807 96 5942 6840 80 5943 1297 20 5944 5832 33 5946 3934 98 5947 7653 45 5948 2712 80 5948 9031 11 5948 6415 79 5949 8332 12 5949 1660 57 5950 8001 28 5951 4057 38 5952 8950 22 5953 376 100 5954 823 26 5955 4450 47 5955 7393 41 5955 670 64 5957 8900 84 5957 7686 38 5957 4168 21 5957 1491 6 5958 9966 60 5958 3194 6 5958 160 84 5959 535 99 5961 6597 72 5961 899 65 5963 6627 21 5963 1506 69 5964 5439 34 5964 6799 45 5964 4336 90 5965 907 48 5965 2615 41 5965 9279 95 5966 8199 13 5966 4468 66 5966 5251 11 5967 637 97 5967 2291 82 5967 1983 94 5967 8338 40 5967 6520 80 5968 2227 88 5968 8188 93 5968 4880 72 5968 9701 35 5969 8364 46 5969 1543 87 5971 4390 69 5971 412 36 5971 7466 87 5972 3571 79 5975 5086 60 5975 4872 4 5976 7798 65 5976 8472 23 5977 7342 32 5978 7008 29 5980 748 55 5980 6037 20 5980 1653 31 5981 2338 90 5982 2808 98 5982 5348 7 5983 584 83 5984 8892 30 5985 1467 21 5985 5501 50 5986 8516 34 5987 7278 12 5988 7463 5 5989 815 19 5990 5975 36 5991 3659 62 5991 9847 18 5993 7511 56 5994 23 21 5995 9092 23 5995 407 91 5996 9964 31 5997 5042 25 5998 9061 20 6000 2029 39 6001 3721 63 6001 9578 40 6002 9525 98 6002 673 42 6005 5510 72 6006 1309 4 6008 5534 35 6009 7092 55 6010 7276 33 6010 9001 24 6011 3854 93 6012 7953 91 6013 353 77 6013 4653 74 6015 551 61 6016 1439 15 6017 1026 11 6020 7686 33 6021 8172 60 6022 7235 73 6022 7798 83 6022 5104 43 6022 5518 50 6023 3832 81 6023 6092 15 6024 5346 64 6024 3908 89 6025 5550 9 6025 2016 57 6025 1223 57 6026 9245 25 6026 9016 92 6026 8665 31 6028 8783 6 6028 2099 81 6029 8281 68 6030 2916 19 6030 5057 91 6030 457 1 6031 2693 44 6031 7462 27 6032 4784 83 6032 7235 92 6033 7486 28 6033 4572 66 6033 7487 14 6033 6327 67 6034 2690 56 6034 2143 60 6035 3008 63 6035 8854 83 6035 1428 100 6037 8504 53 6037 5515 46 6038 7104 64 6038 2085 10 6038 591 73 6039 3672 19 6040 5087 13 6040 815 8 6040 8762 99 6040 6372 38 6041 3341 19 6041 5374 85 6042 6263 5 6042 8273 11 6043 3166 46 6043 9418 41 6044 753 0 6044 6834 15 6044 6853 80 6045 5510 14 6045 1357 79 6047 1921 10 6047 5897 9 6047 4630 4 6049 5993 78 6049 7861 99 6049 8545 8 6049 4389 4 6050 6311 1 6050 4012 46 6050 7328 69 6050 4723 77 6051 9452 59 6051 3356 17 6052 1199 4 6053 3628 8 6054 763 66 6054 5179 50 6055 9195 61 6055 3300 14 6056 2936 98 6057 7361 56 6057 7964 46 6058 5026 82 6059 4940 2 6059 7419 91 6060 1935 24 6060 73 1 6061 7013 2 6062 4043 83 6063 6467 82 6063 7674 43 6063 6243 100 6064 9293 57 6064 2143 90 6064 5212 7 6064 8627 90 6064 1629 88 6065 191 72 6066 2712 19 6067 7492 71 6067 8963 11 6067 3621 88 6068 6337 60 6068 5170 99 6069 659 64 6069 2182 3 6069 1445 86 6069 3639 73 6070 7174 99 6070 864 7 6071 6622 61 6071 8650 95 6072 292 75 6073 9193 44 6073 3834 62 6075 6022 57 6075 5605 6 6075 3429 26 6076 4618 26 6077 898 16 6078 8225 32 6079 8676 60 6079 6671 22 6080 3501 4 6080 1226 47 6081 7249 15 6081 4042 3 6082 5580 61 6082 6336 7 6083 3369 89 6083 1020 51 6083 5826 1 6083 268 73 6084 7275 46 6084 956 83 6084 2747 34 6085 7730 71 6085 3736 65 6085 4049 64 6085 6423 65 6086 9880 90 6087 3915 46 6088 7450 11 6089 8429 86 6092 7272 68 6092 1647 14 6092 7423 98 6092 7428 50 6092 692 60 6093 7438 37 6093 3277 71 6095 7416 15 6096 6151 59 6096 8178 31 6098 388 2 6098 137 7 6100 1143 56 6100 521 86 6100 1878 3 6101 2367 59 6101 9797 39 6101 1611 59 6102 5251 28 6103 3258 80 6106 9987 53 6107 8595 96 6108 8040 45 6109 8191 40 6111 6620 77 6111 2425 61 6112 4346 75 6112 9480 27 6113 4361 9 6113 1510 62 6113 7178 63 6114 7099 8 6114 4273 46 6115 8877 57 6115 504 59 6115 4199 92 6115 1792 6 6115 7574 3 6117 8689 19 6117 3491 89 6118 2308 9 6119 2446 44 6121 3348 71 6121 2703 70 6121 7846 49 6121 4048 23 6122 4601 0 6122 1423 55 6122 8172 33 6123 8206 53 6123 6434 85 6124 2833 47 6127 4513 3 6128 4374 48 6128 4367 36 6129 36 56 6130 6896 6 6130 1470 97 6130 8458 77 6131 3762 26 6132 7295 57 6132 5612 57 6132 4477 71 6132 3523 28 6133 263 26 6135 3425 44 6135 403 17 6135 3095 32 6136 3259 10 6136 9766 21 6137 6573 37 6138 7940 1 6139 5818 4 6139 9663 48 6140 2736 85 6140 45 35 6140 4853 51 6140 6958 20 6140 7843 71 6141 3145 41 6143 8083 11 6143 4716 58 6143 7843 64 6143 6347 85 6144 8148 82 6145 3092 63 6145 5973 83 6146 4441 81 6147 2042 94 6147 8931 85 6147 1198 52 6147 5813 21 6148 4619 79 6148 3368 57 6149 1533 29 6149 177 88 6149 3588 13 6150 6938 33 6151 1363 27 6151 2613 23 6151 4705 27 6152 7926 72 6152 7678 32 6153 9554 77 6154 3875 82 6154 5390 51 6154 7459 3 6155 899 66 6155 2237 54 6155 2763 84 6155 9190 63 6156 6914 58 6158 6080 87 6158 3818 45 6158 6515 55 6160 866 24 6160 728 29 6160 3608 43 6160 6838 88 6161 2339 83 6162 822 12 6163 3382 94 6164 192 48 6164 6915 58 6164 4470 82 6167 4286 78 6167 9502 84 6169 6004 84 6169 8211 78 6169 5807 56 6170 3740 41 6171 108 41 6171 4381 22 6171 1254 23 6173 9798 50 6173 3664 99 6173 7715 17 6173 1278 96 6174 2311 44 6174 4025 71 6174 6342 83 6175 8573 83 6176 750 58 6176 7047 63 6177 8127 41 6178 3590 4 6178 6710 100 6178 1411 56 6180 2254 11 6180 8347 8 6181 3010 30 6182 6110 2 6182 5175 19 6182 8773 37 6183 642 71 6183 9536 94 6183 3677 53 6184 6984 82 6186 7026 7 6190 5832 21 6191 593 100 6191 9923 59 6192 1767 76 6193 749 97 6193 1241 69 6193 3157 26 6194 296 49 6194 7072 86 6195 5109 15 6195 5247 6 6195 8290 20 6196 8462 60 6197 8454 41 6198 2358 55 6198 6875 67 6198 2129 90 6199 6542 59 6199 6140 47 6201 125 7 6201 2644 39 6202 800 44 6203 8644 31 6203 1396 75 6204 6122 44 6204 5287 84 6204 5964 7 6205 42 56 6207 5303 1 6208 7219 32 6209 4796 85 6210 3584 30 6212 5216 0 6212 7808 72 6215 9326 67 6215 8593 48 6217 1475 38 6218 7722 2 6220 4089 81 6220 7296 63 6220 3554 27 6222 9105 64 6224 3354 50 6224 541 100 6226 8879 41 6227 857 32 6227 5993 68 6227 1312 63 6227 4707 57 6228 6185 52 6229 9089 96 6229 3176 28 6229 904 39 6230 363 62 6230 7929 63 6231 6001 7 6233 6822 26 6233 6798 41 6233 5197 77 6233 3813 56 6233 815 18 6234 797 79 6237 2703 60 6237 9047 69 6237 1777 62 6238 782 70 6238 2211 60 6239 8459 58 6241 5197 75 6241 1309 63 6242 4474 0 6244 2530 84 6244 5733 24 6245 715 31 6245 7872 19 6245 7313 7 6245 4940 89 6246 5897 70 6247 1699 38 6248 8131 87 6248 4397 6 6250 4569 28 6252 7107 96 6254 3297 74 6254 6230 52 6254 6274 72 6255 1317 96 6255 6561 100 6256 743 80 6257 9822 45 6257 6028 64 6257 731 73 6259 9303 58 6260 7074 62 6260 2556 23 6263 8139 41 6263 4393 92 6263 3395 38 6264 1173 93 6264 169 26 6265 3958 16 6266 5130 7 6267 4899 38 6268 6702 2 6269 4763 97 6270 4570 29 6270 2726 16 6270 678 62 6270 8199 98 6271 5896 93 6272 1689 65 6272 7181 91 6274 2499 69 6274 1724 27 6274 4421 18 6274 1021 96 6275 8783 41 6275 1253 28 6276 3937 59 6276 7213 82 6276 5649 61 6276 2245 4 6276 9869 89 6277 8951 90 6277 1391 59 6278 9559 81 6279 1264 46 6279 8064 69 6279 4987 94 6280 1591 48 6280 2116 81 6281 9191 55 6283 5621 23 6283 5525 84 6285 6849 70 6285 828 86 6285 902 76 6287 3586 3 6287 9504 87 6289 4658 28 6290 9947 55 6290 809 68 6291 7228 60 6291 853 93 6292 6852 29 6292 6387 66 6294 3398 8 6294 7090 16 6294 9392 65 6298 5654 80 6298 9232 88 6299 9746 97 6299 3129 16 6301 3991 92 6301 6452 90 6301 5226 25 6302 1424 16 6303 5607 46 6303 7946 52 6304 3142 43 6305 7750 95 6305 8183 95 6306 3350 4 6306 3575 17 6307 4570 85 6308 9296 35 6308 1848 73 6311 6735 82 6311 7719 50 6312 8640 99 6312 7111 25 6313 5206 6 6314 3160 30 6314 2803 49 6314 2592 96 6316 7605 60 6316 3473 34 6317 1076 48 6322 9897 69 6323 9663 98 6324 1224 74 6324 574 82 6325 3422 27 6325 575 96 6326 100 71 6326 4296 64 6329 8062 43 6329 3926 20 6329 9858 33 6330 6631 20 6331 1943 47 6331 6882 54 6331 9286 98 6331 7282 89 6332 79 32 6332 645 54 6332 1800 49 6335 3744 52 6336 2862 99 6336 9539 37 6337 7223 51 6337 2065 91 6340 6272 45 6340 228 44 6341 7669 86 6342 6755 14 6342 4002 13 6342 570 20 6342 436 73 6343 284 10 6343 9290 68 6344 5295 75 6344 1946 92 6347 1680 74 6348 7909 69 6349 8603 96 6349 1031 100 6350 2834 83 6351 3692 70 6352 968 27 6352 2880 41 6352 5069 52 6353 2918 18 6353 5387 67 6353 3840 16 6353 5738 18 6354 5390 81 6354 6759 15 6354 689 32 6355 2457 23 6355 6893 69 6355 3131 86 6356 664 91 6356 4742 80 6356 3443 30 6357 5171 64 6358 4367 30 6360 4908 94 6360 6561 35 6360 7897 79 6360 2883 9 6360 9092 42 6361 9396 1 6362 3958 49 6362 2781 58 6363 9060 11 6363 7166 78 6364 1323 41 6365 6049 48 6366 4195 90 6366 8698 31 6366 7853 98 6367 8550 99 6368 1837 44 6369 4412 45 6369 6663 40 6370 4598 45 6370 7271 87 6371 7821 92 6372 9326 1 6372 8035 58 6373 8024 16 6374 3908 25 6374 7529 92 6375 9960 60 6375 6484 19 6375 8931 2 6377 7614 4 6378 1442 44 6378 6149 98 6378 5225 9 6380 7056 57 6380 6089 21 6381 9142 38 6381 7974 58 6382 5063 75 6382 5909 15 6383 7209 42 6384 8160 32 6385 2016 63 6385 9796 76 6385 4681 79 6385 1017 45 6386 9802 97 6386 6038 2 6386 6570 37 6387 7441 69 6388 4849 96 6388 5226 9 6388 3646 76 6388 5430 2 6389 3208 48 6389 7627 23 6390 8083 6 6391 7403 58 6391 2951 69 6392 2912 83 6392 2719 60 6393 1246 49 6393 3260 60 6393 7359 51 6394 4097 72 6396 5103 50 6397 1160 97 6398 6204 13 6398 4535 54 6399 4794 42 6399 4385 6 6400 1849 89 6400 7939 90 6401 2447 15 6401 3845 10 6403 3378 66 6405 2813 71 6406 3593 16 6406 2144 36 6406 559 57 6408 1161 43 6409 7691 90 6409 9254 16 6412 894 95 6414 1209 39 6415 2306 38 6416 1543 38 6416 2839 22 6416 6482 10 6416 6872 11 6417 7545 12 6418 1550 59 6418 617 54 6419 8248 8 6419 8008 18 6421 2688 90 6421 9851 90 6421 5950 29 6421 7891 71 6421 143 11 6421 7170 33 6422 9935 30 6423 7124 85 6423 3499 85 6424 8644 27 6424 4590 54 6425 513 4 6426 4327 28 6426 4470 84 6426 6447 37 6427 1430 39 6427 6793 54 6427 8267 97 6427 3867 90 6427 9731 11 6428 7848 98 6429 9113 71 6429 2230 74 6432 1806 91 6432 2241 20 6432 1142 95 6433 7241 82 6433 3137 35 6433 5782 14 6433 9911 99 6434 8612 26 6434 820 81 6435 7797 36 6436 6380 21 6437 1283 75 6437 9840 52 6439 3231 69 6439 5984 87 6440 7411 65 6441 3897 24 6442 8503 97 6442 5538 2 6442 7219 79 6443 6109 6 6444 5611 68 6444 1500 41 6446 8915 93 6448 4906 74 6448 2305 80 6449 4309 44 6449 5385 82 6449 772 10 6450 8855 88 6452 9388 97 6452 4213 11 6452 1100 37 6454 8306 50 6454 2824 6 6455 9808 78 6455 921 25 6456 4653 88 6457 5814 45 6459 7375 96 6460 5913 51 6461 4560 29 6461 8683 48 6462 3612 97 6464 8237 73 6465 5784 37 6465 7562 52 6465 1092 45 6466 5326 21 6467 9814 38 6467 9343 82 6467 331 66 6468 4573 35 6468 5451 65 6468 6819 99 6468 5277 69 6469 7370 46 6470 3928 74 6472 5279 44 6472 5273 2 6473 3427 32 6475 3317 20 6476 185 38 6476 3273 85 6477 7051 59 6478 3188 36 6478 2641 27 6479 1111 86 6479 3782 19 6480 7129 1 6480 7537 22 6482 204 56 6482 5784 5 6482 1273 8 6483 7221 80 6485 2170 98 6485 6024 66 6485 9583 18 6486 4228 57 6487 3666 45 6487 9064 46 6487 440 15 6487 4977 73 6488 3971 35 6489 5780 37 6489 8220 97 6489 9808 7 6490 3571 5 6491 7114 63 6491 5062 31 6492 8778 77 6494 9568 80 6494 1654 81 6494 2197 67 6494 9297 57 6495 2877 98 6495 8941 5 6497 4192 27 6498 991 93 6499 6850 11 6500 8421 37 6500 9459 15 6501 587 99 6502 1422 63 6504 2001 25 6505 9487 35 6507 6277 95 6508 375 82 6508 1730 59 6509 620 83 6509 8857 65 6509 1439 93 6510 1510 4 6511 7223 13 6512 9647 68 6513 9573 84 6513 3595 9 6513 4388 26 6513 3760 100 6513 4778 36 6514 6308 61 6514 3715 45 6515 3983 67 6516 7828 57 6520 3633 26 6520 7717 50 6520 4484 14 6521 6929 64 6521 5614 19 6522 6286 84 6523 9634 78 6523 2297 93 6523 7674 20 6523 4539 57 6524 9816 23 6524 6634 38 6525 9095 33 6525 7459 81 6525 7625 31 6525 8470 92 6526 8067 10 6527 7469 0 6527 1005 32 6528 9301 57 6528 995 85 6528 6771 35 6528 5712 45 6529 3956 14 6529 5750 59 6530 1184 22 6532 5037 28 6532 2186 84 6533 125 58 6533 2680 12 6533 3569 93 6534 2564 57 6535 549 52 6538 7551 85 6538 2075 57 6539 7107 98 6539 9162 1 6539 6788 13 6539 9996 98 6540 443 23 6540 3130 1 6541 162 15 6543 9770 83 6543 2210 6 6543 8379 19 6545 1767 75 6545 5217 49 6546 1593 45 6546 4603 27 6547 7037 88 6547 123 100 6547 7089 4 6547 9359 17 6548 9275 22 6549 6488 48 6549 7046 68 6550 4736 26 6550 5406 90 6550 559 45 6551 2055 4 6551 226 8 6551 902 3 6551 3474 68 6552 3045 41 6553 1812 76 6554 2853 41 6554 8529 80 6555 9041 51 6555 9982 60 6560 3020 51 6560 9759 3 6560 3697 96 6564 6433 53 6564 1799 2 6564 1201 34 6566 192 94 6566 4482 20 6567 5919 71 6568 778 48 6570 4147 24 6570 4591 45 6571 8458 15 6571 6999 0 6571 7560 46 6572 7081 83 6572 995 59 6572 3596 66 6573 7466 87 6573 3386 98 6574 3693 74 6574 1298 25 6575 6588 70 6575 3882 40 6575 1369 37 6575 6278 69 6575 8346 65 6575 939 74 6576 2801 80 6576 6138 60 6576 8997 55 6576 770 45 6577 860 61 6577 4863 54 6578 7548 65 6578 1702 64 6579 3694 88 6579 4264 52 6580 7936 70 6582 3851 77 6582 1381 68 6582 6681 91 6583 6808 16 6583 1911 44 6583 5458 86 6583 9325 14 6583 2919 12 6583 8891 14 6583 9326 15 6584 5023 69 6584 853 44 6585 9395 79 6586 2783 25 6586 5371 79 6586 8607 26 6587 1673 69 6587 7076 96 6587 20 51 6587 7078 5 6589 1024 45 6589 5517 42 6589 6104 19 6590 6486 79 6590 6361 43 6591 4688 70 6592 2215 29 6592 8668 91 6592 5707 68 6592 2552 95 6593 9105 62 6593 8991 54 6593 8258 48 6594 232 59 6594 2800 27 6594 3503 85 6596 1776 74 6596 5986 11 6596 2902 45 6597 8605 98 6598 6755 0 6598 2259 23 6598 5151 74 6598 9512 43 6599 7929 7 6599 940 47 6600 7284 84 6600 1651 64 6601 3203 21 6601 6934 13 6602 5396 9 6605 7817 88 6606 5342 15 6606 5519 84 6606 7409 98 6606 8727 37 6607 2252 62 6608 45 55 6608 6717 58 6608 8149 69 6609 1400 10 6610 8021 13 6611 6283 63 6612 8576 78 6612 3654 50 6613 4499 30 6613 3052 45 6613 3087 1 6613 8666 73 6615 193 72 6615 2173 99 6615 9329 65 6616 7925 21 6617 5362 100 6617 3486 95 6617 6450 20 6618 7706 52 6618 1265 61 6619 4171 44 6619 288 98 6619 6960 64 6620 3388 82 6621 6188 23 6621 9205 60 6621 4103 32 6622 7687 16 6622 4996 37 6623 2822 100 6624 4463 78 6624 9526 98 6624 8733 68 6626 5527 73 6626 1650 37 6629 5860 69 6629 5402 34 6629 2904 3 6630 1332 49 6630 7442 41 6631 3 90 6631 606 82 6631 9014 34 6631 2216 65 6631 8022 60 6632 2564 33 6634 3389 40 6638 7528 67 6638 6594 84 6639 5589 27 6639 5892 43 6639 6920 75 6640 1820 49 6640 819 72 6640 6205 73 6640 5150 48 6641 8842 67 6642 740 91 6642 3603 4 6643 2428 59 6643 638 59 6644 1986 42 6645 5794 41 6645 9645 88 6650 6911 81 6650 592 89 6650 770 24 6651 9720 75 6651 1347 72 6652 8190 31 6653 6227 38 6653 6893 98 6653 4600 74 6655 8124 46 6656 3232 68 6656 4404 47 6656 8170 34 6656 3323 58 6657 1489 87 6657 4643 100 6660 5944 99 6660 4472 87 6660 2099 21 6660 2389 84 6660 3362 17 6662 6089 10 6664 2973 25 6664 9840 97 6665 9605 17 6665 3224 93 6666 4168 73 6666 8500 18 6667 5509 44 6667 7688 97 6667 8699 81 6669 9970 77 6669 9543 0 6670 4734 69 6670 8917 47 6670 1115 79 6670 511 62 6672 6142 78 6672 6901 76 6672 2696 6 6673 2721 92 6673 6874 87 6673 8857 77 6673 444 59 6675 3279 94 6676 2554 52 6676 8453 30 6677 4414 10 6677 9521 52 6682 5113 85 6682 6633 27 6683 4106 99 6684 8391 48 6684 774 64 6685 1383 38 6685 2154 26 6685 1792 12 6686 7218 29 6686 5119 37 6690 1019 54 6690 5167 26 6692 4673 82 6694 2575 57 6694 5094 58 6694 8284 35 6696 9971 25 6697 8265 27 6697 833 77 6698 9972 53 6699 834 61 6700 5735 11 6700 7081 64 6701 7361 10 6701 5991 86 6702 7844 82 6702 7235 44 6703 7947 69 6703 8233 8 6706 8941 30 6706 3263 85 6707 7224 68 6708 2104 5 6709 4252 57 6710 8842 70 6712 5843 84 6714 5513 60 6714 1386 54 6715 3222 42 6716 7423 82 6717 5217 63 6718 6473 28 6718 6145 14 6718 6367 77 6718 8938 93 6720 2351 29 6721 2714 96 6722 1380 40 6723 5586 11 6724 5440 30 6725 4390 78 6726 7064 55 6726 7434 32 6727 7191 77 6729 5636 59 6730 1058 4 6731 1607 35 6731 3424 26 6732 2067 86 6732 7559 66 6733 9650 55 6734 2195 74 6734 5284 99 6734 994 69 6737 3680 13 6737 6931 94 6739 5740 45 6740 2534 33 6740 6193 56 6740 7288 78 6741 5997 25 6741 165 97 6741 5127 47 6741 3518 50 6742 7831 38 6742 8476 21 6742 2488 81 6743 3345 61 6744 4991 93 6745 5494 53 6746 5300 80 6746 9795 63 6746 2093 19 6747 3387 51 6747 5651 34 6748 8735 25 6748 8695 32 6749 7120 80 6750 4602 45 6750 9717 58 6750 6495 38 6750 9462 32 6751 288 13 6752 6382 50 6753 3326 45 6753 1197 1 6753 1047 42 6753 4014 88 6753 9813 45 6754 3123 56 6755 6793 9 6755 7974 77 6756 591 91 6756 6243 67 6756 7764 11 6757 53 63 6760 9595 18 6760 7361 72 6760 7724 34 6760 3035 24 6761 7071 49 6761 2464 11 6763 4073 55 6764 1235 59 6764 4316 2 6765 7680 80 6765 678 0 6766 7646 23 6767 4740 16 6768 6668 52 6768 2266 3 6768 9981 72 6768 9380 81 6768 5935 50 6768 4163 4 6772 9719 25 6773 886 23 6773 2300 56 6774 4002 70 6775 9514 65 6775 5247 52 6775 9479 79 6775 5069 58 6777 2216 47 6778 3495 86 6779 760 67 6780 7273 6 6780 1341 80 6780 1104 95 6780 7622 74 6783 3119 63 6783 178 4 6783 7712 88 6784 8462 65 6784 4610 99 6787 3949 29 6788 7548 93 6788 9804 30 6788 7666 21 6788 3258 85 6789 1249 72 6789 9939 11 6789 2079 36 6794 1106 58 6794 310 97 6794 9568 4 6794 9917 55 6794 258 44 6795 269 71 6795 5629 78 6796 398 25 6797 1798 39 6797 2323 73 6798 7732 98 6799 5565 5 6799 9155 83 6800 1253 61 6800 8556 77 6802 8542 64 6802 6508 19 6803 9370 42 6804 2409 49 6804 5762 10 6805 1149 14 6805 1265 51 6806 7000 75 6806 8275 63 6806 44 83 6806 3382 14 6807 8930 40 6807 5471 98 6808 2347 6 6808 2061 39 6808 2505 79 6810 950 12 6811 3496 79 6812 4391 9 6813 6330 46 6814 4646 88 6815 1562 78 6815 8840 16 6815 6526 22 6816 1296 47 6816 4144 23 6817 5884 66 6819 8687 6 6819 3401 66 6819 968 24 6819 5219 60 6820 7061 54 6820 2718 23 6822 7349 62 6822 7366 29 6822 4886 86 6824 3821 63 6824 3997 94 6825 3249 23 6825 4886 43 6826 8404 86 6826 3551 59 6826 6523 63 6827 2091 36 6828 3103 24 6828 5660 88 6829 4378 25 6829 6901 49 6830 3824 50 6830 6749 64 6830 9151 78 6832 3310 63 6833 6580 79 6834 1597 45 6834 4591 13 6834 3934 92 6835 1769 79 6836 9914 91 6837 435 96 6837 6853 0 6837 4334 99 6838 6430 2 6840 173 67 6840 2355 94 6841 3679 57 6841 280 48 6842 6352 34 6842 5225 2 6842 9854 11 6843 4030 18 6844 4401 80 6844 8344 59 6845 7017 36 6845 9852 48 6846 5724 67 6847 4049 86 6848 7944 67 6848 205 91 6849 2286 15 6849 9919 47 6850 2896 31 6851 3872 94 6851 7112 17 6852 99 55 6852 8945 62 6853 6655 35 6853 3353 77 6853 9796 37 6854 6022 84 6854 1684 94 6855 6550 85 6855 3360 55 6856 5024 98 6857 2030 94 6858 8097 33 6858 4191 46 6859 7155 6 6860 7774 6 6860 1760 42 6860 5848 42 6861 9097 51 6861 2056 42 6861 8303 63 6862 1344 34 6862 1016 25 6862 9094 34 6863 9420 49 6863 6616 10 6863 9933 32 6864 96 37 6864 6540 25 6866 1605 10 6867 9557 55 6868 4640 75 6869 995 72 6870 1383 49 6870 5200 27 6871 1668 35 6871 2365 58 6872 4966 12 6872 9099 38 6872 3781 48 6872 5681 26 6873 5989 44 6873 336 36 6875 8323 18 6876 668 50 6876 7507 53 6877 5124 26 6878 447 27 6878 1536 82 6878 1484 2 6878 577 30 6878 6375 50 6880 2512 86 6881 1998 93 6881 4276 20 6882 9727 55 6882 2383 26 6882 7920 56 6883 4840 34 6883 4956 87 6884 1622 9 6886 730 5 6886 6177 65 6887 5836 36 6887 3188 28 6887 210 89 6888 1179 100 6889 4905 79 6889 4099 39 6890 8587 67 6891 6404 27 6892 5579 4 6892 3734 33 6893 6000 73 6895 9823 70 6895 8526 80 6896 4843 22 6896 9496 93 6896 7713 22 6896 5215 3 6897 3052 17 6898 5000 30 6898 4310 92 6901 9059 68 6902 5622 74 6903 679 44 6904 3651 28 6904 1147 76 6905 8037 35 6905 5938 83 6905 1203 83 6906 4768 23 6907 1855 94 6908 7293 80 6910 6662 33 6910 8089 67 6911 2692 57 6912 4200 22 6915 5197 38 6915 4100 14 6915 7185 96 6916 6495 65 6919 5472 5 6919 7746 83 6919 6242 55 6920 7981 71 6921 4372 75 6921 9317 48 6921 159 66 6923 8379 16 6923 8599 97 6924 1613 60 6925 5935 65 6925 4796 17 6927 192 3 6928 120 32 6929 6847 69 6930 5856 36 6931 4543 12 6932 9573 38 6932 1540 35 6932 2808 11 6934 7338 11 6934 5219 60 6935 8399 15 6936 1920 87 6937 4231 58 6937 2132 69 6938 1092 29 6938 8772 91 6940 4686 54 6940 1659 95 6941 5737 2 6942 5586 95 6942 8148 27 6943 6053 3 6944 5863 8 6944 8656 88 6945 1032 84 6945 5858 11 6946 3734 37 6946 9445 71 6947 3849 21 6948 3109 54 6949 6117 5 6950 1051 89 6950 904 59 6950 1401 72 6951 5687 89 6951 7667 14 6952 686 58 6952 2604 69 6952 2252 3 6952 5728 4 6953 2788 11 6957 1746 60 6960 5870 76 6960 6260 26 6961 8020 8 6962 7430 82 6962 4880 37 6963 8849 22 6964 5623 87 6964 5656 24 6964 9729 54 6964 4793 51 6964 2456 20 6965 3763 16 6968 3755 99 6969 7462 97 6969 5846 2 6969 7738 3 6970 2959 22 6970 712 27 6972 3576 38 6973 7848 24 6973 0 39 6974 9521 29 6975 747 60 6975 1963 14 6975 9445 65 6977 239 2 6978 887 12 6979 7269 95 6979 3901 19 6980 9939 2 6980 1342 46 6981 4485 39 6981 8879 49 6981 8551 12 6982 6849 1 6983 627 41 6983 2420 40 6983 1511 12 6983 8644 60 6983 2743 67 6984 8956 42 6984 2028 67 6986 7448 42 6988 8711 45 6988 544 100 6989 5536 18 6990 4605 100 6991 6705 67 6992 6567 42 6992 8244 67 6992 3220 6 6992 2358 68 6993 9261 21 6994 5949 80 6994 3821 26 6995 5148 17 6997 9543 59 6998 5844 21 6998 7436 62 6999 6648 7 6999 5272 100 6999 301 57 6999 3640 69 7000 5152 3 7000 1954 84 7000 2068 1 7000 9506 88 7001 4823 99 7002 2868 42 7002 581 38 7002 3851 57 7004 3521 31 7006 7355 34 7007 9136 24 7007 2334 46 7007 1100 86 7007 8222 1 7007 209 38 7007 5442 17 7007 962 7 7008 1793 16 7008 4930 100 7008 6486 98 7009 9898 59 7009 7344 69 7010 1372 22 7012 9763 88 7013 6346 64 7013 4962 76 7013 1304 62 7014 2979 64 7014 2297 72 7015 6866 64 7015 754 58 7015 3795 10 7016 896 75 7017 4615 100 7018 8587 70 7018 1368 48 7019 6571 54 7019 8034 12 7020 5917 54 7020 7834 83 7021 1523 19 7022 252 44 7022 5233 93 7023 9145 43 7023 6684 20 7023 5126 77 7024 6110 86 7025 6964 63 7026 8349 17 7027 1042 4 7027 1660 71 7027 6138 29 7028 8430 25 7029 4224 12 7029 4712 79 7032 8998 96 7032 1638 37 7033 3495 86 7033 5852 40 7034 8540 48 7034 3352 75 7034 172 93 7034 7212 24 7034 8781 38 7035 4331 50 7035 4433 3 7035 2798 44 7035 3533 2 7035 7616 94 7036 7640 2 7036 7146 73 7037 3199 74 7037 9934 90 7038 2441 17 7039 3757 39 7040 8952 46 7040 2668 6 7040 3005 67 7041 58 29 7041 2507 11 7043 2643 89 7043 7236 35 7045 8629 30 7046 9299 80 7047 1244 61 7047 9194 65 7047 9181 59 7047 9567 79 7048 6949 51 7048 8004 50 7049 4560 65 7050 5249 45 7050 3079 36 7050 8167 24 7051 1445 27 7052 1754 54 7053 5977 34 7053 70 63 7054 8769 72 7055 1916 8 7055 919 88 7056 2131 14 7057 253 90 7057 4610 45 7058 2245 61 7059 7616 52 7060 271 52 7061 5630 97 7061 7913 66 7062 287 8 7062 404 12 7062 7157 29 7063 2234 52 7063 545 99 7063 6513 54 7063 8086 67 7064 3724 53 7064 602 28 7066 4158 31 7066 5978 40 7066 1875 96 7067 6998 81 7067 2538 36 7067 1267 87 7067 9258 58 7068 5797 35 7069 1430 10 7070 9496 39 7070 3693 93 7071 6763 10 7072 8396 31 7072 9914 4 7073 5722 71 7074 9707 67 7074 8244 82 7075 9137 100 7075 2023 76 7076 3322 65 7076 8567 65 7076 2380 66 7077 7152 27 7079 4068 60 7079 6359 10 7079 7964 85 7080 744 34 7080 3692 23 7081 6690 85 7081 910 13 7081 9857 62 7081 2749 66 7081 3600 21 7082 7351 61 7083 1885 56 7083 4033 0 7083 2610 75 7085 3472 75 7086 5640 64 7087 5610 44 7087 9009 100 7087 426 27 7088 4415 92 7089 7411 83 7089 7574 66 7090 7743 10 7090 4952 33 7090 4304 0 7091 2765 91 7092 7768 55 7092 8688 7 7093 6080 21 7093 5545 50 7095 7793 96 7099 605 97 7099 4733 47 7100 4881 50 7101 990 23 7101 6501 17 7103 2092 47 7104 989 33 7104 5867 54 7106 6205 74 7106 2756 42 7107 3895 83 7108 6190 53 7109 3853 41 7109 311 2 7109 8114 70 7110 7088 15 7111 7743 73 7111 6151 42 7112 9336 52 7113 4057 66 7113 3232 67 7115 8008 82 7115 3448 62 7115 1848 37 7116 5803 4 7116 9397 56 7116 3427 55 7117 4473 68 7118 7056 33 7118 1274 97 7119 9901 35 7119 5465 31 7122 3032 38 7122 9822 97 7122 975 13 7123 9066 9 7123 9605 62 7124 4794 8 7124 2821 13 7124 2608 41 7125 2559 57 7126 3652 62 7127 7423 81 7128 3280 12 7128 7120 84 7129 3746 91 7130 8864 66 7131 1193 89 7131 3181 48 7132 2033 0 7132 6825 66 7132 9711 78 7134 3255 46 7134 4341 33 7135 2616 38 7135 4520 34 7136 4923 79 7136 493 45 7137 9223 82 7137 9677 97 7138 5983 93 7139 1815 74 7139 683 39 7142 9865 36 7143 6782 100 7143 1293 10 7144 5276 60 7146 3792 62 7146 4297 75 7147 6879 70 7148 5229 74 7149 3509 87 7150 1685 22 7150 9723 8 7151 500 84 7153 1566 40 7155 260 37 7156 9010 35 7156 9682 19 7157 6537 95 7157 6446 85 7157 6691 1 7158 2941 34 7158 1555 7 7159 8394 79 7160 8886 18 7160 2128 57 7160 8139 7 7162 920 55 7164 6776 56 7164 7110 9 7165 8435 78 7166 4184 66 7166 1657 4 7167 9104 29 7167 2269 33 7168 1200 40 7169 7900 24 7170 1019 31 7170 7575 32 7172 9605 69 7173 6850 32 7174 8334 26 7174 2332 52 7175 816 30 7175 5645 15 7177 12 56 7177 3532 15 7179 7135 22 7181 5936 22 7183 7467 8 7184 214 85 7184 7985 97 7184 818 52 7185 2997 29 7185 9051 98 7185 9756 75 7185 7029 27 7185 8808 28 7186 9750 61 7188 1535 92 7188 6839 28 7189 4275 77 7189 3760 99 7190 9450 94 7192 4428 17 7193 5670 86 7194 6082 68 7195 3371 20 7195 5190 61 7196 5400 20 7197 4909 17 7197 9953 0 7199 2912 14 7199 6442 50 7200 5006 36 7200 2235 14 7202 4751 67 7203 1303 6 7203 7999 95 7206 2759 52 7206 2768 37 7206 5174 71 7208 2170 43 7208 8099 40 7209 6050 98 7211 8081 47 7211 2338 78 7211 9392 21 7212 3669 23 7215 6993 86 7216 1719 79 7216 1559 22 7216 8732 24 7217 2110 21 7217 7366 67 7218 9865 29 7218 2985 11 7218 9045 8 7219 7434 56 7220 8557 26 7220 7116 56 7221 3060 64 7221 9389 82 7221 1496 6 7222 1089 31 7222 2035 9 7222 9198 29 7226 9192 68 7226 8921 36 7226 6425 94 7226 603 53 7227 1472 85 7229 8114 13 7231 532 11 7233 5938 2 7234 5378 89 7234 7260 83 7234 7470 10 7234 9706 29 7235 7867 36 7236 1306 9 7236 5792 17 7236 9748 16 7236 6074 30 7238 4688 90 7239 2500 22 7239 8768 19 7239 7480 23 7241 4077 86 7241 86 39 7242 7298 54 7242 2629 84 7243 1060 36 7243 3034 80 7244 5293 73 7245 5680 98 7245 3848 50 7246 7422 72 7246 1246 62 7247 8291 35 7248 6763 14 7248 9376 99 7249 45 18 7249 6989 72 7250 359 36 7250 7826 25 7251 4903 48 7251 9115 52 7253 963 98 7253 2406 99 7254 2184 97 7256 4017 14 7258 260 67 7259 2817 100 7260 3291 50 7260 1379 29 7261 4164 3 7261 6480 37 7263 4682 63 7263 7005 47 7263 3282 28 7264 9437 95 7265 2910 53 7265 1573 83 7267 782 29 7268 280 50 7270 2710 82 7270 8072 64 7271 8613 63 7273 2019 50 7274 1569 60 7274 8369 41 7275 2203 92 7276 5543 57 7276 7717 46 7276 5968 59 7276 2507 37 7277 1099 13 7277 8919 33 7277 2098 31 7278 2136 14 7279 386 67 7279 4470 9 7279 5278 42 7280 5481 71 7281 9880 2 7282 6019 48 7283 3939 90 7283 8330 90 7286 3969 71 7286 8271 66 7287 4753 63 7289 8461 61 7289 3070 100 7290 4477 94 7291 5788 45 7293 8521 63 7294 5708 46 7294 5430 33 7294 8648 49 7295 3636 33 7296 7036 64 7296 7474 31 7297 8664 52 7298 2459 75 7299 2387 66 7299 7492 31 7300 6063 14 7300 9509 0 7301 1173 62 7301 9548 39 7302 6119 57 7302 4533 96 7304 7439 100 7305 461 64 7305 4587 95 7305 9887 17 7306 5015 31 7306 8655 37 7306 1805 75 7307 3460 50 7307 9728 16 7307 6058 57 7307 5525 40 7308 7081 58 7308 9175 56 7309 2631 86 7309 4205 64 7311 9486 44 7312 3308 41 7312 9008 62 7312 7406 11 7313 6698 23 7313 4395 8 7314 6484 37 7315 2302 38 7316 7538 63 7316 2811 85 7318 1951 24 7319 9858 99 7321 5005 100 7321 1523 73 7323 1022 40 7325 626 67 7326 8335 53 7326 8595 12 7327 819 25 7327 3891 65 7328 931 47 7330 5766 77 7330 4851 68 7333 7634 100 7335 4349 77 7336 5090 48 7337 3814 37 7338 7802 29 7340 5204 99 7341 9073 83 7341 4069 23 7342 9454 69 7343 3834 67 7345 4221 91 7346 3131 88 7347 8324 57 7347 3043 4 7349 1660 3 7349 3157 54 7349 6111 2 7350 2900 98 7350 3641 30 7351 3501 57 7351 6353 55 7352 5126 91 7352 9441 70 7353 1793 44 7354 2515 8 7354 701 14 7354 2844 51 7354 1561 31 7355 9155 68 7355 5267 14 7356 5324 79 7359 9637 15 7359 989 66 7360 2453 47 7360 4340 23 7360 6132 27 7362 1210 42 7362 9168 73 7362 508 28 7363 3051 34 7363 9743 4 7363 9403 95 7363 9685 41 7365 1597 47 7365 5949 79 7366 2330 98 7366 7362 79 7366 2690 57 7367 3559 43 7368 4999 8 7368 6062 61 7369 8040 12 7369 1837 5 7369 477 80 7370 4582 73 7370 4441 12 7370 7642 46 7372 6118 24 7372 94 18 7372 1626 21 7374 2435 99 7374 6928 36 7375 3886 14 7375 11 38 7375 2306 55 7376 2323 65 7377 1083 92 7378 8988 59 7378 4491 80 7379 8105 65 7379 1821 40 7381 5089 32 7381 4692 57 7381 834 39 7381 8947 49 7381 9835 45 7381 3478 67 7383 7979 5 7385 9741 68 7385 6150 18 7386 3160 86 7386 7409 26 7388 7198 73 7388 7985 78 7389 9681 16 7390 3655 76 7390 8632 85 7392 9553 70 7394 7441 91 7395 4911 88 7396 3291 92 7396 3664 51 7396 9995 0 7397 914 85 7397 5186 20 7398 3781 19 7399 3074 70 7399 9186 3 7400 602 38 7400 2557 52 7400 6441 84 7402 4608 1 7402 2116 42 7403 7171 68 7405 818 42 7405 2375 56 7407 1836 41 7407 7801 68 7407 8083 92 7407 9878 88 7407 9326 42 7409 3728 59 7410 1863 33 7411 5414 55 7412 9265 75 7412 9222 41 7415 9863 27 7417 4506 34 7417 702 36 7418 7585 92 7418 5687 0 7422 8858 42 7422 694 78 7423 9976 34 7425 1426 68 7426 3587 49 7426 2743 14 7427 8183 25 7428 6887 59 7430 9600 85 7430 842 26 7431 4790 39 7431 9666 57 7433 4450 94 7435 6541 45 7436 9976 87 7437 8035 47 7438 2185 61 7440 3859 8 7440 4854 84 7441 8857 92 7441 9436 99 7442 4161 63 7442 4032 85 7444 1871 43 7444 4752 88 7444 319 40 7445 960 49 7447 6287 100 7447 1417 16 7448 4760 77 7448 5433 86 7449 1298 80 7449 7256 18 7449 7996 5 7450 128 82 7450 7355 25 7451 7123 7 7451 2283 1 7451 6317 47 7451 6446 34 7452 5736 99 7453 7614 95 7453 5384 79 7454 907 69 7454 6996 87 7454 8915 73 7456 3804 9 7456 9967 66 7456 4229 19 7458 9351 95 7458 5767 3 7460 3130 57 7460 2291 65 7461 2862 19 7462 1971 48 7462 4727 22 7463 5210 54 7463 1186 9 7464 7673 17 7464 150 70 7464 2873 26 7465 8780 33 7466 9312 85 7467 1359 14 7467 2083 96 7467 6984 11 7467 9798 27 7468 1370 81 7469 927 9 7470 1409 47 7474 2323 100 7475 812 79 7475 8788 70 7476 5763 100 7476 1955 83 7477 641 0 7478 7416 71 7479 5461 5 7480 5376 79 7480 5757 100 7480 4845 44 7480 4497 39 7480 4194 4 7482 4368 88 7483 3282 74 7483 4911 46 7485 3402 68 7485 4262 6 7486 3079 96 7487 5665 13 7487 3901 20 7488 2282 78 7490 4560 15 7491 8043 13 7491 880 89 7492 446 85 7492 4546 32 7494 7163 34 7494 8018 96 7495 2049 44 7495 7427 39 7497 8940 43 7497 8397 31 7498 8118 32 7498 60 64 7500 2924 16 7502 9464 92 7502 6424 69 7502 7186 37 7504 4597 15 7505 9701 98 7505 8345 49 7505 1871 47 7506 2034 78 7506 961 26 7506 4896 58 7507 3810 5 7508 8453 55 7508 3071 73 7508 2293 16 7508 5594 96 7509 6389 98 7509 5968 84 7510 2535 78 7510 1820 58 7511 9078 96 7511 9303 43 7511 1977 20 7512 275 37 7512 3007 61 7515 9721 1 7515 8521 11 7516 496 51 7517 4923 62 7518 6905 17 7519 9086 10 7520 4438 34 7521 5713 84 7523 5271 59 7525 8576 88 7527 5110 47 7528 2918 44 7529 1788 81 7531 5349 94 7532 2085 34 7532 2653 53 7533 3684 55 7533 7732 22 7533 8302 16 7535 3637 6 7535 2522 87 7535 5856 36 7535 2327 48 7535 848 29 7535 7091 18 7536 3841 57 7536 2812 21 7536 391 84 7537 7952 29 7537 556 100 7537 7601 31 7539 7053 32 7539 2793 21 7540 6912 97 7541 1191 56 7541 3792 56 7542 5981 69 7542 5597 95 7542 68 31 7544 390 61 7545 8481 20 7546 7385 66 7546 7161 38 7548 2791 58 7548 3403 49 7548 2208 66 7548 944 13 7548 9420 99 7550 5004 76 7551 2359 94 7554 7240 2 7554 8575 85 7555 268 3 7556 1432 13 7556 7606 94 7557 1635 13 7558 3091 61 7559 8208 52 7559 6386 57 7559 8573 97 7560 7745 13 7561 9179 65 7562 6103 43 7562 5473 93 7562 6487 100 7563 8717 43 7563 3466 91 7564 8181 85 7564 6526 9 7564 163 26 7565 4706 64 7565 6787 14 7566 174 62 7567 9421 8 7568 2915 44 7568 8497 40 7568 2323 6 7568 7786 90 7569 2722 16 7570 3032 49 7570 1978 89 7571 3437 93 7572 1240 25 7573 743 59 7573 2842 63 7573 9682 95 7574 4438 98 7575 4032 95 7575 807 19 7576 6132 76 7577 1019 89 7578 9733 83 7578 5569 35 7579 8532 49 7580 4483 37 7580 3193 26 7581 405 48 7581 460 90 7582 762 32 7583 2648 93 7584 8327 37 7584 1041 41 7584 9043 39 7585 6529 88 7586 4329 26 7587 3144 89 7587 6767 27 7587 3512 20 7587 7758 56 7588 3713 7 7588 9726 12 7588 5442 77 7589 6344 45 7590 386 75 7590 5041 84 7591 1792 88 7591 3960 12 7593 7820 98 7593 3398 57 7594 1154 15 7594 6039 71 7594 8887 7 7594 2443 75 7595 9743 91 7595 3210 46 7595 3985 15 7595 1670 66 7596 8388 96 7597 8886 90 7599 8037 97 7599 5178 0 7600 2014 12 7600 1711 27 7603 2604 99 7604 3654 93 7605 6159 11 7605 5144 72 7605 2721 62 7605 3898 93 7605 4739 34 7606 5435 47 7607 8491 47 7608 7610 80 7608 3743 72 7610 1098 39 7611 2366 32 7612 5512 85 7613 6937 19 7613 490 92 7616 9672 81 7616 9530 84 7617 3810 72 7617 4686 63 7618 9611 96 7618 2420 88 7620 2358 10 7621 9642 57 7622 8402 86 7622 2285 22 7624 7074 2 7624 1309 58 7626 898 37 7626 6229 19 7626 3192 69 7627 6946 26 7627 7853 12 7628 7566 40 7628 2604 76 7628 2910 91 7629 4345 16 7630 1882 77 7631 2457 29 7631 1127 97 7632 9638 11 7632 8148 58 7633 150 4 7634 3736 33 7635 3381 62 7638 4929 23 7638 1784 48 7638 8725 77 7641 3846 20 7641 7955 52 7641 8426 80 7642 1595 72 7642 5661 94 7642 6981 51 7642 618 35 7642 1900 17 7643 1390 4 7643 1578 16 7645 3960 97 7645 2083 45 7645 5445 54 7646 191 26 7647 9319 15 7648 2346 95 7648 2088 81 7649 9041 82 7650 179 22 7650 7175 91 7651 2610 87 7652 5338 56 7653 3443 22 7654 8207 21 7654 432 89 7655 8172 8 7657 1899 25 7658 6684 34 7660 4938 65 7661 5379 91 7661 1224 77 7662 2095 2 7662 6764 29 7662 5994 2 7663 4372 40 7664 4738 15 7665 6816 2 7667 3765 48 7667 8366 66 7667 5167 88 7668 4401 23 7669 6517 75 7669 9473 49 7669 8483 91 7672 1134 3 7672 9598 96 7673 4213 94 7674 8885 13 7674 8428 91 7674 4115 96 7675 7871 96 7675 7786 90 7676 5780 71 7677 1475 18 7677 8988 38 7677 739 68 7677 1030 23 7678 6002 13 7678 5833 53 7678 5690 48 7680 2116 56 7681 1684 11 7682 6720 38 7683 7014 25 7683 9102 60 7684 2250 27 7685 340 99 7685 8662 9 7685 4479 1 7685 8501 55 7686 3572 2 7687 1873 13 7687 405 7 7688 7799 16 7688 7203 73 7688 356 32 7689 1164 17 7689 2900 61 7690 7427 90 7690 9720 13 7691 2262 84 7692 3941 10 7692 2561 88 7692 2498 77 7694 8007 65 7694 435 46 7694 3561 65 7694 2558 94 7695 6741 60 7695 3413 45 7698 5679 100 7698 1844 33 7700 5263 44 7703 8638 57 7704 7385 86 7705 9371 88 7706 4500 99 7706 1993 28 7707 7854 23 7710 973 77 7710 5572 46 7711 149 88 7712 3892 15 7713 3608 45 7713 4845 75 7713 6231 49 7714 1819 98 7715 7296 14 7715 9630 19 7716 8597 88 7718 8190 59 7718 4720 33 7718 7618 12 7718 257 12 7719 415 16 7721 1396 89 7723 6001 43 7723 4492 19 7724 7625 81 7725 4438 37 7726 9121 71 7728 1755 78 7729 6995 66 7729 1248 65 7731 7471 60 7731 698 78 7731 2728 50 7731 7836 50 7732 2790 21 7732 7591 65 7733 2778 47 7734 7989 76 7734 520 71 7734 7228 14 7734 1777 53 7735 6430 41 7735 2404 36 7736 1653 83 7738 2010 26 7738 2976 58 7739 875 64 7739 3854 8 7740 687 93 7741 1169 54 7741 3274 94 7742 6257 37 7742 9331 93 7743 3085 68 7743 2960 98 7743 529 18 7743 9856 33 7744 1808 75 7744 626 17 7745 733 10 7745 6925 69 7745 1820 97 7747 5190 34 7747 5590 82 7750 6110 33 7750 3920 47 7750 7268 82 7750 5330 74 7751 4266 71 7752 8708 56 7753 3598 92 7753 8043 29 7754 6869 54 7755 4955 9 7755 7889 16 7756 6725 94 7757 1690 48 7757 3272 77 7758 7340 0 7758 2203 40 7758 444 41 7759 785 3 7760 264 18 7761 9761 48 7761 8495 45 7763 1194 20 7763 8168 77 7765 3862 94 7766 1772 98 7767 3929 61 7769 5413 79 7769 6400 5 7769 7014 45 7769 2642 9 7770 2449 86 7770 8673 54 7770 5720 47 7770 9615 53 7770 9505 100 7771 6720 87 7772 163 39 7772 6513 51 7772 1776 64 7773 4172 48 7774 3265 27 7774 4839 70 7774 151 39 7775 3708 16 7775 9356 93 7776 8913 90 7776 7952 46 7777 2674 20 7778 53 55 7778 7698 77 7780 3411 44 7781 9255 53 7781 615 97 7783 7611 92 7784 6494 24 7785 610 36 7785 7292 46 7786 7954 36 7788 6584 88 7792 1847 31 7792 1806 40 7792 1182 78 7792 45 82 7792 126 4 7793 7142 43 7793 1308 1 7794 5629 32 7794 2622 86 7795 8185 67 7796 6359 31 7797 8378 74 7799 6858 1 7800 8253 64 7802 1691 33 7802 2510 57 7802 2322 75 7803 9106 1 7804 1774 55 7805 7139 77 7807 3203 47 7808 5025 48 7808 2589 96 7809 3995 67 7810 2652 23 7810 600 36 7810 6507 31 7811 5110 58 7812 2043 80 7812 3463 26 7813 9752 13 7814 2987 64 7816 8123 33 7817 6932 53 7817 5081 14 7817 6314 100 7818 8434 18 7819 1417 48 7819 9122 52 7821 5770 53 7823 4503 37 7824 9367 12 7824 5559 38 7826 4873 46 7826 9772 2 7826 9782 5 7828 5471 21 7828 8169 27 7828 5360 51 7828 2927 14 7828 298 8 7829 7171 21 7829 2218 99 7830 1219 97 7832 2568 100 7832 1574 22 7833 4172 47 7833 8747 4 7833 3145 33 7833 8055 85 7833 199 64 7834 6262 9 7834 2030 33 7836 8821 46 7836 9666 41 7836 2170 73 7836 4669 43 7837 6556 87 7837 833 63 7838 8449 2 7838 3270 77 7838 3383 63 7839 2329 24 7839 6204 10 7840 6658 75 7840 2696 68 7841 5612 32 7841 1853 14 7842 7595 55 7843 2436 54 7844 7740 49 7844 7295 6 7844 7396 51 7845 785 73 7845 4373 54 7846 1208 100 7846 6385 24 7846 4442 96 7847 889 54 7847 624 99 7849 9660 98 7849 2870 57 7849 5479 74 7849 8674 19 7850 755 55 7850 9366 23 7851 3870 46 7852 3904 52 7853 1467 90 7854 2728 81 7854 9852 53 7854 2143 71 7854 4364 25 7854 4715 34 7855 2553 13 7856 3194 76 7857 2756 10 7857 1036 51 7857 2540 11 7858 9906 38 7858 1618 39 7859 9760 43 7860 9767 24 7860 9269 31 7860 615 59 7861 9545 52 7861 6271 37 7861 3967 22 7861 4406 49 7861 8547 33 7862 8227 75 7863 2896 32 7865 4175 100 7865 6974 77 7866 4384 91 7866 4029 81 7867 6063 27 7867 4722 17 7868 8435 17 7868 7299 42 7868 7163 26 7869 2553 84 7869 7332 75 7870 9580 89 7870 7036 15 7872 929 7 7873 672 84 7873 2117 46 7874 116 92 7875 9069 56 7875 79 64 7875 9455 45 7875 6684 76 7876 4123 28 7876 6004 56 7876 5094 2 7877 4389 36 7878 3629 44 7879 463 6 7879 3480 37 7879 9889 2 7879 5206 89 7880 7070 72 7880 4004 79 7880 8333 8 7881 6082 60 7882 8220 82 7882 4788 87 7882 2554 1 7883 6092 13 7883 3465 35 7883 1537 25 7883 6635 82 7884 7974 95 7884 8617 92 7885 6296 96 7886 1579 2 7887 7963 27 7887 1883 46 7888 1236 47 7888 8833 74 7889 9033 63 7889 3546 10 7889 3114 18 7890 2994 84 7890 5812 5 7891 8835 30 7892 4810 56 7892 6971 100 7893 6139 23 7895 1243 78 7895 3626 38 7896 4449 67 7897 9267 59 7897 2580 18 7898 6175 61 7898 6991 65 7898 2322 98 7898 3813 50 7899 7303 85 7900 5496 45 7900 7827 35 7901 6855 23 7902 2318 0 7903 2165 92 7904 8356 80 7905 1100 37 7906 6289 85 7909 3913 91 7911 325 13 7912 6439 41 7912 5764 70 7914 8380 71 7914 1380 32 7914 9390 34 7914 7197 44 7914 4670 43 7915 3010 11 7916 1723 94 7916 264 100 7917 4226 65 7917 5121 50 7918 5785 32 7918 2262 35 7919 9038 56 7919 4671 36 7919 3861 35 7920 3471 29 7920 4425 31 7921 3279 25 7921 3417 44 7922 4829 27 7922 5571 20 7922 8120 68 7923 7421 62 7923 7117 38 7926 5237 63 7927 6753 71 7927 8127 95 7927 5620 10 7930 6889 79 7930 6878 15 7930 4509 84 7931 7917 6 7932 376 86 7932 7784 68 7932 3061 56 7933 7338 74 7933 906 51 7933 9309 24 7934 5335 21 7934 2301 82 7935 8963 51 7936 1817 69 7937 1631 25 7938 1458 66 7939 9120 67 7939 5751 100 7940 9917 66 7941 8790 50 7941 6585 26 7941 5019 32 7942 8802 42 7942 8205 37 7943 7105 43 7944 6915 33 7944 1342 33 7946 4026 71 7946 2943 72 7946 1473 82 7947 9146 76 7947 5571 88 7949 7713 16 7950 6440 22 7950 53 6 7951 3608 90 7951 4888 96 7952 9088 80 7952 1641 96 7952 993 80 7953 5460 6 7953 8903 55 7954 6811 50 7955 1267 57 7956 9384 84 7956 2863 83 7958 359 79 7959 1030 63 7959 4566 19 7959 1089 6 7960 9168 39 7960 1090 60 7960 2350 68 7961 8241 96 7962 2375 0 7962 4508 76 7963 1012 63 7964 1956 95 7964 4249 74 7966 8393 32 7967 8859 37 7967 4038 100 7970 9887 90 7971 1356 33 7971 7673 23 7971 5387 94 7972 8500 28 7973 783 29 7973 2659 10 7973 8168 53 7974 6881 10 7975 9291 9 7975 7043 26 7979 8246 27 7979 9939 74 7980 2891 22 7980 1336 21 7981 2495 25 7981 1183 28 7982 4405 28 7982 9189 80 7983 3578 32 7985 7947 43 7985 4648 73 7985 5217 94 7986 979 85 7987 9085 30 7988 6060 85 7988 7666 92 7990 9305 64 7990 594 71 7992 5578 49 7993 889 58 7993 3273 45 7994 5436 20 7995 3928 17 7995 2854 40 7995 9046 85 7996 820 14 7999 7435 22 7999 4038 68 8000 3397 12 8000 1624 5 8000 1170 51 8000 4212 62 8001 9992 85 8002 6130 93 8004 8040 94 8004 1861 98 8004 414 80 8005 225 55 8005 3758 96 8007 5628 68 8008 1887 27 8008 389 11 8008 9436 40 8008 3386 69 8009 5060 94 8010 4803 74 8010 1740 56 8010 5797 7 8012 3987 48 8014 342 0 8015 5483 1 8015 9549 68 8015 2346 70 8016 9320 77 8016 7956 60 8017 4527 66 8017 6445 38 8020 7463 23 8020 506 67 8020 6221 18 8021 468 98 8021 3795 17 8023 635 31 8023 1189 84 8023 5457 9 8024 3936 96 8027 7104 37 8027 3375 3 8027 8926 23 8027 2489 89 8029 3600 96 8031 7114 98 8032 8586 31 8032 6486 58 8032 9938 0 8032 9656 3 8033 4981 95 8034 4650 14 8035 5140 55 8036 9513 42 8036 2997 83 8036 7771 87 8037 2551 36 8037 6298 18 8037 6455 41 8037 9930 85 8039 7495 19 8039 2500 38 8042 6242 18 8043 9556 42 8043 1544 79 8043 5988 39 8044 4939 85 8044 4619 64 8044 4895 66 8045 3667 98 8045 2004 19 8045 4255 98 8046 8946 2 8047 7717 42 8047 8741 36 8048 2208 80 8048 5645 63 8048 5897 9 8049 3825 78 8050 4226 41 8052 4777 79 8052 4970 41 8053 4543 72 8054 9067 1 8055 9369 77 8055 3862 86 8056 2831 39 8058 1940 20 8058 6307 63 8059 1493 90 8059 4283 63 8060 4602 96 8060 7076 74 8061 1484 48 8062 9616 7 8062 2513 70 8062 7823 9 8062 5652 16 8063 7254 46 8063 7996 79 8063 6480 69 8064 4247 93 8066 254 93 8067 3293 20 8067 4267 18 8067 1883 93 8069 4035 25 8072 7106 18 8072 6685 37 8072 4622 76 8072 7565 35 8073 7688 50 8073 7438 51 8074 8949 40 8074 8855 39 8074 5643 33 8075 1337 2 8078 4013 42 8079 5653 66 8080 4860 36 8080 2334 27 8080 3721 62 8081 6000 37 8081 3497 83 8081 5036 20 8082 5745 37 8082 4502 9 8082 3360 77 8084 8157 67 8084 7649 60 8085 8293 14 8085 859 70 8086 2000 7 8086 4858 95 8086 9720 2 8087 7163 30 8089 9809 65 8089 5596 42 8089 4505 47 8090 4155 17 8092 5227 17 8092 4058 56 8093 7400 82 8095 4715 11 8095 3941 56 8096 6317 46 8098 5119 18 8098 7629 73 8099 8312 75 8100 3671 73 8101 9264 52 8101 2250 0 8101 892 86 8101 5899 26 8102 2577 27 8102 9919 34 8102 5552 35 8104 9331 18 8105 3783 12 8106 5950 38 8106 8945 27 8106 5099 45 8107 3422 56 8107 2817 70 8108 7974 42 8109 9048 78 8109 1262 22 8109 3477 85 8110 4627 39 8111 2352 79 8112 8489 33 8112 1863 68 8113 3749 15 8113 3860 36 8114 2800 65 8114 9844 22 8115 6218 79 8117 1981 66 8118 558 37 8118 340 93 8119 3622 68 8121 3546 72 8121 1670 37 8121 5660 56 8122 3917 41 8123 5833 51 8124 120 62 8127 3658 66 8127 55 47 8127 7491 72 8127 7405 82 8128 9214 26 8129 6471 6 8129 3033 14 8129 730 95 8130 3467 21 8131 7740 8 8132 8954 0 8132 4718 91 8133 9744 68 8134 7262 83 8134 6097 59 8137 7675 96 8137 1110 70 8137 6690 24 8139 9348 2 8140 2476 1 8140 6833 53 8141 2495 7 8141 8570 50 8142 6038 69 8143 6548 28 8144 7319 9 8144 6892 10 8144 6172 86 8145 9925 88 8146 2044 35 8146 2430 90 8146 7375 70 8147 7297 49 8147 214 23 8148 9076 53 8148 6441 16 8149 1404 93 8150 6698 28 8152 4675 15 8152 4360 29 8152 2850 12 8153 7824 95 8154 7808 69 8156 8259 79 8156 7869 51 8157 6613 89 8157 1500 70 8158 3251 44 8158 6845 74 8159 3811 64 8159 8892 37 8159 5353 67 8159 1289 39 8160 5702 35 8163 8549 49 8163 579 93 8164 7876 3 8164 1412 0 8166 3349 58 8166 4448 100 8166 5174 60 8167 2789 97 8167 4369 16 8167 941 62 8168 3060 75 8169 3604 40 8170 4475 55 8170 6538 14 8171 3259 93 8171 5010 87 8173 6286 65 8173 6205 76 8174 5087 49 8175 8374 6 8177 6421 49 8178 4120 70 8178 1210 66 8180 8702 12 8180 1146 5 8181 4247 15 8182 433 29 8182 7700 41 8183 8875 88 8183 9551 90 8183 6609 73 8186 7780 70 8187 9297 29 8188 3622 82 8188 1661 13 8190 8697 29 8190 2470 38 8190 6908 51 8192 3500 94 8192 1385 33 8193 2602 69 8194 6514 19 8197 520 33 8197 6237 15 8197 2007 85 8198 3619 13 8198 3261 53 8199 3503 83 8199 1390 39 8200 4068 60 8200 6796 99 8201 4357 47 8202 7684 36 8202 665 60 8203 2711 11 8203 9024 18 8203 4452 37 8204 4757 87 8204 8046 31 8204 3632 1 8205 5732 93 8205 6459 86 8206 8766 55 8207 2584 46 8208 5463 42 8209 4623 91 8210 4169 5 8210 3626 89 8210 9242 90 8211 9471 19 8211 2413 17 8212 9048 85 8212 2335 98 8212 7918 61 8213 7808 98 8213 6853 40 8213 1030 57 8214 6719 34 8214 7260 62 8214 4183 11 8214 188 0 8215 7715 5 8216 1050 2 8216 1930 95 8216 7207 37 8217 8574 41 8217 9203 89 8218 4391 83 8219 2917 71 8219 3467 82 8219 8494 72 8221 2142 8 8222 2342 46 8224 387 4 8224 3130 2 8224 6163 65 8224 6420 67 8224 1877 14 8225 7687 5 8226 7214 23 8227 262 44 8228 2718 100 8228 4358 71 8229 876 27 8230 4693 43 8230 8833 14 8231 3499 47 8231 9887 1 8231 1888 66 8231 8703 77 8233 6092 84 8234 3241 50 8235 3511 74 8235 3711 26 8235 8512 3 8235 8398 54 8237 7241 60 8237 6073 36 8237 8919 20 8237 6755 59 8238 5722 64 8239 3589 22 8240 8152 9 8241 8699 50 8241 9557 60 8242 6432 69 8244 1642 50 8244 7509 7 8244 4595 86 8244 7614 29 8244 8660 77 8245 6522 45 8245 8357 12 8246 5352 85 8247 9583 36 8247 3499 10 8250 9358 17 8251 260 55 8251 3731 63 8252 4306 93 8252 9975 31 8254 9445 99 8254 7208 64 8255 4967 55 8256 8184 94 8256 5839 100 8256 4254 20 8256 2081 24 8257 6074 53 8257 5195 11 8258 3471 58 8258 6797 8 8259 3507 53 8259 6760 49 8259 6483 22 8260 6578 37 8260 9358 8 8261 5822 34 8262 7116 34 8262 1443 91 8262 1535 64 8263 1065 16 8263 6262 74 8263 5254 20 8266 7644 63 8266 9573 63 8268 3913 26 8270 3722 14 8272 288 67 8272 4522 29 8272 9402 12 8273 4372 57 8273 4619 22 8275 5862 86 8275 5444 3 8276 1047 8 8277 8448 79 8278 983 8 8278 2896 2 8278 8697 14 8279 1747 80 8280 2428 26 8280 7955 35 8281 3235 20 8283 8038 24 8283 817 20 8284 5270 41 8286 7664 32 8286 8741 91 8287 9333 34 8288 1465 54 8288 3197 90 8288 1529 12 8288 4056 75 8289 2998 48 8290 5041 39 8291 173 13 8291 7917 35 8292 2550 49 8292 7251 58 8292 7164 28 8293 90 30 8293 566 46 8294 5165 47 8294 9857 91 8294 7849 57 8296 5052 33 8296 5699 47 8297 262 49 8298 2801 91 8299 3827 47 8303 9767 19 8304 1816 16 8305 2146 81 8306 6544 19 8306 5120 58 8307 6348 45 8308 2964 75 8308 8959 21 8308 398 20 8310 7006 11 8311 3287 60 8312 717 79 8312 9868 23 8313 1976 18 8314 5412 90 8315 8600 14 8315 5939 65 8315 7144 58 8315 2852 1 8316 3936 84 8317 8468 40 8317 6826 17 8317 1244 92 8317 9058 54 8318 8892 34 8318 2577 88 8320 2935 82 8320 7196 62 8320 1000 23 8321 732 32 8322 5175 48 8322 9424 69 8323 6358 78 8325 8810 65 8326 9993 74 8326 761 54 8326 6268 49 8327 3729 56 8328 7745 78 8330 3394 32 8331 7112 34 8331 714 32 8331 8394 95 8334 1372 70 8334 742 11 8336 1961 32 8337 284 54 8337 9677 30 8338 4708 27 8338 4341 26 8338 8517 3 8338 3082 40 8339 5518 63 8340 427 75 8340 312 99 8342 2175 27 8342 256 39 8343 4382 22 8343 18 45 8345 6659 81 8346 8558 28 8347 880 86 8347 5061 9 8347 6289 44 8347 6704 45 8348 9468 62 8349 5889 19 8349 3670 78 8352 5310 61 8353 8797 10 8356 1764 15 8358 390 30 8358 7580 54 8358 2101 20 8360 4238 86 8360 8114 21 8360 2195 31 8360 3512 100 8361 5256 36 8362 6659 59 8363 2720 36 8363 6294 66 8364 9055 50 8364 6277 86 8364 5835 19 8366 4878 30 8366 9620 28 8367 7918 94 8368 4549 58 8368 9547 65 8369 1170 16 8369 386 2 8371 2255 88 8372 5068 2 8372 2564 61 8374 3273 29 8375 183 91 8375 9867 65 8376 3681 73 8376 3419 36 8376 7124 72 8376 6538 62 8377 7875 75 8378 8650 71 8378 1333 1 8379 9092 75 8379 7080 82 8381 3159 0 8382 6768 43 8383 4599 5 8383 7187 74 8383 5284 45 8384 9143 20 8385 6061 12 8385 1958 83 8385 4930 98 8385 2895 75 8387 4647 58 8387 8437 84 8388 8288 22 8388 7427 96 8388 8372 96 8389 2020 88 8390 9489 12 8390 7106 87 8391 951 85 8391 9168 23 8392 2415 28 8393 3865 11 8393 8224 62 8393 6986 64 8394 797 14 8394 8703 95 8396 4539 46 8396 3451 59 8397 368 61 8397 259 89 8398 8603 1 8399 742 32 8400 1955 20 8400 2942 88 8401 3177 15 8402 4795 16 8402 912 7 8403 1825 50 8403 3689 4 8406 3049 89 8406 81 29 8410 2518 39 8411 173 74 8412 7502 76 8412 6799 40 8412 5038 62 8413 8948 77 8415 321 43 8415 2920 20 8415 4925 51 8415 5165 65 8415 7314 91 8415 5025 51 8416 8181 80 8416 7453 81 8417 5258 8 8417 1373 100 8417 5189 66 8418 789 49 8420 3257 25 8420 3637 30 8422 6352 81 8422 8667 81 8422 1205 87 8424 1632 91 8425 534 1 8426 2558 81 8427 6424 21 8427 5749 77 8427 6781 62 8429 5777 37 8430 3577 52 8430 8470 7 8430 968 88 8430 8518 60 8431 5071 2 8432 832 49 8432 3831 96 8432 5514 74 8433 9451 17 8433 1829 7 8434 1964 18 8435 5715 75 8436 6144 31 8436 1486 45 8437 1008 54 8438 6641 39 8438 3765 0 8438 8601 21 8438 283 74 8440 3992 30 8440 2623 33 8440 5051 30 8440 157 84 8442 8682 5 8442 2337 28 8442 5886 96 8443 5179 17 8443 3228 84 8444 8438 78 8444 9515 87 8445 9679 22 8446 4887 86 8446 5094 71 8447 6103 26 8448 1859 48 8448 9391 78 8449 1772 53 8449 7729 19 8450 7236 59 8450 3720 0 8450 9381 32 8451 3693 42 8452 9286 88 8453 5609 6 8454 4533 97 8455 8529 84 8455 2476 86 8455 3686 79 8456 3180 95 8456 2296 77 8457 7720 17 8458 2423 77 8458 7378 99 8458 6047 10 8459 7747 7 8460 3525 8 8460 5638 95 8460 7844 36 8460 9909 16 8461 9788 98 8461 4676 60 8461 5698 87 8463 6296 76 8464 6201 15 8465 3400 55 8466 5214 14 8466 2494 81 8467 6721 50 8467 965 22 8468 1452 79 8469 6150 35 8469 283 62 8471 271 0 8472 780 1 8473 4847 31 8475 1747 87 8475 9159 99 8476 2678 1 8477 551 92 8478 5986 54 8478 4577 86 8478 3724 17 8480 5860 18 8481 6294 10 8481 3416 87 8481 5843 100 8481 6780 13 8482 2807 1 8483 5813 53 8484 1094 8 8484 7063 77 8484 6260 52 8484 3961 29 8485 7671 82 8486 756 55 8487 1057 97 8488 4325 43 8488 2943 12 8489 9306 69 8489 1716 74 8489 5514 40 8490 8118 44 8491 2990 79 8492 4047 90 8493 2076 98 8493 8768 7 8494 4714 97 8495 7323 33 8496 1425 44 8496 3955 38 8497 7514 60 8497 7667 86 8498 4110 58 8498 2977 31 8499 1698 15 8500 1260 70 8502 1868 71 8503 8609 69 8503 3013 44 8504 997 62 8504 2481 92 8505 5636 59 8505 7412 32 8507 5902 1 8507 9926 6 8508 3537 60 8508 7391 39 8508 1481 61 8508 971 67 8509 1575 87 8509 4823 51 8509 9674 1 8509 2040 33 8511 7092 31 8511 1665 32 8511 6565 10 8512 5709 84 8513 8591 11 8514 326 100 8514 4740 83 8514 9832 50 8515 1694 21 8516 4431 39 8516 3148 43 8518 6094 44 8518 4026 11 8519 5029 26 8520 5776 68 8520 5045 53 8521 1428 50 8521 9162 85 8522 817 100 8522 4871 23 8523 6777 31 8523 6719 19 8523 9225 66 8524 4080 0 8524 2177 30 8525 7589 5 8525 6982 68 8526 2065 94 8526 2870 13 8528 6487 8 8528 4388 49 8528 1106 5 8530 3576 43 8532 760 86 8533 7174 59 8533 541 16 8534 3833 78 8534 7140 16 8535 6549 7 8535 593 10 8535 5737 63 8538 1640 87 8538 113 94 8539 1883 8 8540 8043 49 8542 9623 24 8542 5382 84 8542 920 67 8542 7373 55 8543 2816 10 8544 505 0 8545 5198 95 8546 3327 91 8546 5437 28 8546 5850 77 8547 3858 53 8547 1706 27 8549 7807 58 8552 959 52 8552 938 38 8553 5303 84 8554 6356 63 8554 590 99 8554 1276 77 8556 1985 78 8556 6863 0 8557 1773 77 8557 2589 1 8558 8117 37 8558 9163 9 8558 8544 50 8559 8966 78 8559 2107 67 8560 3262 91 8561 2588 55 8563 7302 29 8563 6662 56 8563 5756 0 8564 633 27 8565 1207 96 8566 9661 66 8567 2456 58 8567 4001 28 8567 83 70 8568 609 58 8569 1583 93 8569 2667 0 8569 6772 86 8570 6070 73 8570 2832 2 8570 9599 99 8571 9820 13 8572 4297 44 8573 192 4 8574 2915 73 8575 5955 16 8575 2472 78 8576 7398 43 8576 1267 47 8577 8052 69 8578 9192 12 8579 7603 1 8579 6439 54 8582 8161 35 8582 1052 61 8583 3359 2 8584 922 20 8585 257 36 8586 2169 72 8586 1889 71 8587 824 36 8588 505 21 8588 6734 95 8588 1629 10 8589 4668 65 8589 1834 26 8589 1441 78 8591 192 39 8592 32 30 8593 4468 88 8593 7365 23 8595 4960 71 8595 4951 48 8596 8194 80 8596 503 18 8597 5845 57 8598 4582 48 8600 7996 56 8601 6217 95 8601 2146 78 8601 5175 36 8603 196 82 8604 9481 40 8604 6394 64 8604 8349 26 8604 1434 33 8604 8790 47 8605 4052 12 8606 9373 25 8606 4050 71 8607 778 73 8607 2357 29 8607 801 3 8608 8194 78 8608 6337 66 8611 2733 17 8613 8004 14 8615 2601 24 8616 3016 70 8617 3796 97 8618 8445 76 8618 2769 77 8618 712 100 8619 7940 32 8619 2090 22 8620 8558 16 8620 5903 38 8620 7878 90 8621 537 80 8622 3326 41 8622 3264 22 8624 1866 16 8624 2177 73 8626 7867 55 8627 1481 26 8628 4383 5 8628 2066 95 8628 7188 83 8629 4224 98 8630 3254 41 8630 2718 92 8630 2880 62 8631 1865 37 8631 989 1 8631 4900 72 8632 4077 21 8632 586 59 8632 3081 66 8632 2361 85 8636 9627 76 8638 2240 69 8639 7418 15 8639 7410 2 8640 9509 57 8640 4102 63 8640 1921 13 8642 632 96 8642 663 10 8643 1505 83 8643 2209 78 8644 6516 20 8645 2725 49 8646 7819 95 8646 5577 66 8646 296 48 8647 1572 33 8647 1105 49 8647 8304 2 8647 8936 72 8648 4169 9 8648 117 26 8648 8682 55 8649 5792 47 8650 4538 73 8650 5617 23 8651 5470 53 8653 8829 7 8653 4346 17 8653 1167 1 8653 9769 24 8654 2214 71 8656 4800 19 8656 21 27 8656 922 75 8657 4519 47 8658 9797 90 8658 1605 89 8659 9036 2 8659 7507 59 8660 3756 70 8660 4978 19 8661 5842 30 8662 4405 99 8663 7424 99 8664 1468 25 8665 9350 67 8665 9683 99 8666 6074 100 8668 6700 55 8670 7588 50 8670 5113 11 8672 9239 99 8673 9757 76 8673 2320 34 8673 236 4 8673 7398 84 8674 2088 78 8675 8175 83 8677 9762 81 8678 6673 92 8678 1251 79 8682 1070 28 8682 6033 57 8683 9222 18 8683 9427 90 8684 6840 49 8685 3771 76 8685 9058 95 8685 3899 38 8686 3865 0 8686 3044 45 8688 1510 80 8691 2101 55 8691 4098 65 8691 7891 28 8693 4202 54 8694 6924 9 8696 5760 80 8696 7105 55 8696 9213 91 8697 3245 24 8697 8905 1 8697 1969 42 8697 3650 82 8698 698 48 8698 8000 100 8699 6993 19 8699 834 2 8700 5501 6 8700 7744 15 8700 710 79 8700 8837 84 8700 2976 14 8701 658 27 8702 4112 95 8702 6746 94 8703 4236 45 8704 5700 6 8705 1883 23 8705 9705 63 8705 3679 76 8706 8511 15 8707 9604 15 8708 9424 75 8708 3637 58 8709 8433 41 8710 3392 18 8710 2799 44 8711 3410 11 8711 2301 65 8711 3051 3 8714 9448 43 8714 2938 1 8714 8538 11 8714 6276 55 8715 1278 69 8716 6993 52 8717 7490 89 8720 6462 99 8720 3510 56 8722 6541 52 8723 1250 92 8723 8528 63 8724 720 25 8725 3705 39 8725 2420 51 8726 2947 83 8727 5663 46 8727 8193 83 8728 9041 25 8728 6727 71 8728 963 77 8728 3046 65 8728 983 99 8730 2625 13 8731 1493 96 8731 7614 1 8731 3751 51 8731 3148 43 8732 7014 99 8733 3832 91 8733 6947 91 8734 7464 62 8734 3460 47 8735 2117 22 8736 7865 39 8736 6351 69 8736 7276 67 8737 6665 88 8738 2345 75 8740 2019 59 8740 3250 34 8741 2811 36 8741 457 19 8741 4331 91 8741 521 59 8743 9625 28 8743 4102 28 8743 5250 82 8744 4365 78 8745 4567 3 8745 7712 58 8745 2044 60 8746 3242 8 8746 7539 10 8748 9281 31 8749 9744 38 8749 7697 92 8749 5369 41 8752 8916 37 8752 4024 67 8752 4333 89 8752 4567 71 8753 8361 73 8753 2479 6 8754 816 98 8754 1170 33 8754 8026 29 8754 5755 6 8757 5746 23 8757 232 44 8758 324 42 8759 5401 90 8759 748 88 8760 621 36 8760 4549 50 8760 7051 18 8761 483 98 8762 4017 4 8762 5758 24 8762 4372 97 8762 7550 56 8763 843 44 8763 2838 18 8764 2078 25 8765 2584 41 8767 2360 46 8767 2474 95 8768 8609 48 8768 3393 62 8768 3535 16 8768 1310 89 8768 4731 24 8769 2060 18 8769 351 68 8772 214 77 8772 6555 76 8773 6936 70 8773 1097 96 8774 4786 91 8775 8212 73 8776 4982 70 8777 6779 76 8778 4609 2 8778 4498 67 8779 1130 82 8779 459 64 8781 5003 47 8781 9297 30 8781 2618 65 8781 6961 96 8783 6171 30 8783 2387 39 8785 8585 64 8786 2714 46 8787 3905 36 8787 7347 40 8788 4762 2 8789 411 42 8789 9738 7 8789 4058 92 8791 4277 87 8792 9606 8 8794 5579 47 8795 4570 9 8796 2899 86 8796 437 26 8799 9404 9 8799 9957 53 8799 6895 81 8800 1061 98 8800 6857 66 8800 1342 49 8801 6879 4 8803 4997 95 8803 1270 77 8803 4609 19 8804 2551 99 8804 4468 87 8804 1464 16 8804 4057 70 8805 4221 87 8805 7621 69 8807 6375 7 8807 5424 55 8807 8815 64 8807 1021 75 8808 567 51 8808 7504 17 8810 1137 5 8810 9832 98 8811 5620 79 8811 2573 80 8811 7321 57 8812 5105 29 8812 727 8 8813 645 36 8813 6874 2 8815 7571 29 8815 1100 7 8816 3757 87 8817 433 86 8817 1481 2 8817 4395 57 8818 4296 36 8818 5224 82 8819 461 54 8819 5034 6 8820 2469 64 8821 6435 32 8821 4110 20 8821 983 77 8822 1450 55 8822 9210 62 8823 9988 90 8823 9241 73 8823 9681 40 8824 610 42 8825 189 12 8826 2152 84 8828 6601 73 8829 6091 16 8830 1086 27 8830 6181 58 8832 7069 18 8835 5271 81 8836 5242 100 8836 8524 12 8837 9573 33 8840 3185 23 8840 2902 46 8841 2551 86 8841 5018 6 8841 4887 12 8841 8144 74 8841 7903 39 8842 6171 71 8843 6230 77 8843 6597 53 8843 9537 8 8844 3173 76 8844 328 25 8845 7205 74 8846 834 19 8846 8368 17 8846 9138 10 8847 2909 38 8847 2983 89 8848 3274 64 8850 685 59 8850 1746 94 8850 3818 54 8851 4176 78 8851 9130 24 8851 5600 16 8852 7407 55 8852 3978 68 8853 4129 70 8853 121 58 8854 4557 77 8854 8630 65 8854 8130 3 8855 3586 23 8855 7911 36 8858 9496 28 8858 3049 93 8859 7459 66 8860 8660 25 8861 4879 88 8863 4264 33 8863 8841 41 8865 3887 79 8866 5425 100 8866 3963 2 8867 2893 46 8867 1401 2 8870 748 58 8870 3502 91 8871 4329 30 8871 2117 51 8872 5521 22 8872 6130 65 8872 3626 47 8873 2380 59 8874 5097 41 8876 1473 25 8878 6413 10 8878 4699 97 8878 367 9 8878 5500 22 8878 900 26 8882 7688 56 8883 833 50 8884 4207 44 8884 1119 48 8885 7636 98 8886 890 62 8886 1105 50 8886 5446 13 8887 6512 18 8889 7646 32 8889 2159 26 8890 5501 57 8890 3043 33 8891 4349 43 8891 4098 96 8892 8682 25 8892 9104 40 8893 4548 54 8894 1813 33 8895 9765 67 8895 7344 79 8896 6661 69 8896 69 72 8898 7054 34 8900 1538 81 8900 7975 58 8902 1614 69 8903 5970 98 8904 6646 0 8905 8773 40 8905 4706 57 8906 1888 97 8906 6900 95 8906 2696 8 8908 8117 6 8909 215 9 8909 8829 94 8909 6212 39 8910 179 52 8911 2913 83 8912 7102 84 8912 3726 36 8912 3463 7 8912 9896 50 8913 989 79 8913 3980 23 8914 9166 27 8914 5617 46 8916 9333 85 8916 2656 64 8917 7683 60 8917 6145 21 8917 1027 63 8918 4098 80 8919 9161 20 8919 2251 44 8920 3047 26 8923 3458 57 8923 4472 21 8923 1985 44 8924 7383 6 8926 6957 74 8926 4905 26 8928 4368 2 8929 4247 58 8929 7216 26 8929 6490 32 8930 8488 31 8932 7090 72 8932 2041 33 8933 8249 46 8934 5664 70 8934 370 94 8935 2673 25 8935 8112 28 8937 5623 36 8938 9550 91 8939 9617 88 8941 3971 5 8942 4929 4 8943 2847 54 8944 4288 66 8945 5145 44 8946 2226 94 8947 7710 70 8948 6627 86 8948 8636 84 8952 8566 48 8952 7438 63 8952 6907 17 8952 2577 3 8953 1025 50 8954 5386 8 8955 6211 65 8955 8318 3 8955 3577 38 8955 1894 50 8957 7167 36 8958 4915 89 8959 9661 83 8959 6904 93 8959 968 59 8959 7575 6 8960 7358 37 8961 9630 39 8962 3586 19 8963 6392 81 8963 9967 74 8963 5473 36 8964 2188 77 8964 2856 57 8968 2162 60 8969 5576 33 8969 112 98 8969 736 46 8970 8532 16 8970 3271 59 8970 5757 80 8971 4329 74 8971 3982 88 8971 6395 22 8972 6770 56 8973 931 66 8974 3568 32 8975 5386 29 8977 8359 90 8977 909 61 8979 9930 90 8979 309 18 8980 1627 56 8980 4079 98 8981 8606 59 8981 5825 12 8981 580 4 8982 8716 17 8983 9372 85 8984 775 76 8984 9915 30 8985 5126 34 8986 8555 58 8986 7093 94 8986 7200 97 8988 1092 60 8990 9552 100 8990 1152 52 8991 6513 68 8991 2944 14 8992 9482 78 8992 787 98 8992 593 75 8992 6859 64 8992 3840 24 8992 9562 73 8993 5709 4 8993 4519 55 8996 5399 85 8998 6224 18 8999 3219 17 9000 5877 44 9000 6769 57 9001 6158 17 9001 8316 44 9002 3987 96 9002 6593 11 9002 9704 100 9003 6697 94 9004 1266 6 9004 5414 95 9004 8411 64 9005 8273 49 9006 1603 43 9006 4335 78 9007 7450 42 9008 8495 29 9009 9208 29 9010 2806 62 9011 4703 35 9011 2276 18 9012 9715 88 9013 3704 83 9015 2136 88 9016 2468 62 9016 5375 6 9019 6544 12 9021 6151 43 9021 3394 61 9022 4441 86 9022 3767 38 9023 7432 6 9023 1858 28 9023 7211 17 9024 3589 83 9026 7939 46 9027 605 6 9027 2933 50 9027 6344 76 9027 5081 96 9028 3601 10 9028 4273 51 9028 5772 80 9028 5921 87 9032 8711 77 9033 2682 46 9035 9196 37 9035 1550 24 9035 233 18 9036 43 22 9037 1408 40 9038 5431 98 9038 9226 19 9038 1594 13 9039 6824 81 9039 1791 3 9040 5313 26 9040 7456 11 9041 5940 62 9042 6913 44 9043 4994 29 9043 1497 33 9044 1550 2 9044 1855 7 9046 9402 57 9046 2563 75 9047 1306 78 9047 8951 18 9048 6059 16 9048 5967 72 9049 2119 58 9049 1958 59 9050 1737 54 9050 4827 68 9050 7436 46 9051 8015 50 9051 6251 98 9051 7438 3 9051 6756 14 9052 8842 68 9052 8890 96 9054 2167 45 9056 2431 83 9056 2839 44 9057 5412 8 9057 1971 52 9057 9422 56 9058 4590 62 9059 4655 14 9059 2422 44 9062 2420 52 9063 4289 21 9063 7643 40 9064 8182 6 9065 2274 76 9066 8433 50 9066 5059 28 9067 7165 9 9069 8703 7 9069 5325 75 9069 9412 0 9069 2011 10 9070 6587 56 9070 3261 41 9073 7584 91 9073 6289 82 9073 3934 0 9073 9921 47 9073 7070 62 9074 4017 43 9074 9915 30 9075 7129 82 9076 5461 32 9076 2622 77 9077 7898 30 9077 3004 69 9078 9950 56 9080 8608 23 9080 6732 50 9081 7337 12 9082 7960 9 9082 913 50 9082 3595 12 9083 5048 44 9084 2890 68 9084 5781 78 9085 6421 46 9085 8339 32 9086 4807 71 9086 7596 25 9086 9136 72 9088 1863 87 9088 2249 53 9089 5503 5 9089 9482 82 9089 4319 89 9089 3078 7 9090 381 10 9092 3862 34 9093 8351 50 9093 7850 45 9094 8353 45 9095 1691 65 9095 3979 74 9096 3677 80 9096 2800 16 9097 7134 82 9099 2729 92 9099 7526 8 9100 4093 55 9101 9454 28 9102 7056 11 9103 2530 56 9103 8172 19 9105 788 82 9105 6590 21 9106 8798 65 9106 3844 88 9107 217 79 9107 8878 34 9107 8575 27 9108 2038 68 9108 9847 65 9110 6682 13 9111 3533 4 9111 2934 13 9112 228 68 9112 2608 59 9113 6087 67 9113 6347 9 9114 8467 65 9114 6505 83 9116 3162 52 9117 5682 21 9117 5464 79 9117 716 84 9118 4153 48 9120 9793 8 9121 4042 24 9124 6847 12 9124 2057 26 9126 5780 96 9128 1073 93 9128 635 17 9129 4016 93 9130 8573 15 9132 4907 14 9132 7022 62 9132 61 57 9134 719 23 9134 2646 17 9134 5092 0 9135 4426 13 9136 8905 61 9136 5788 52 9136 3678 90 9136 1476 95 9138 8617 91 9138 4753 62 9139 1063 98 9139 6882 86 9140 3268 42 9142 1568 44 9142 8778 13 9143 377 97 9144 4948 80 9146 5461 76 9146 2390 74 9146 3271 68 9148 5605 19 9149 6592 16 9149 2519 100 9151 4081 7 9153 9164 2 9154 9187 6 9154 4760 9 9155 9147 96 9155 3647 23 9156 2676 34 9158 5897 4 9159 1329 75 9159 2723 35 9160 9980 73 9160 3340 17 9161 7418 30 9161 2420 90 9161 2005 81 9163 9835 39 9163 8453 83 9164 9706 54 9164 3210 98 9165 4951 89 9165 9477 44 9165 5608 17 9165 2661 50 9166 2524 35 9167 1334 66 9168 4780 19 9170 3477 39 9170 1477 63 9171 1956 15 9171 2513 64 9171 8865 73 9172 4357 46 9172 2487 44 9173 7169 26 9174 4506 87 9174 5778 20 9175 278 90 9178 306 94 9178 2845 60 9179 4575 24 9179 8161 56 9179 754 28 9179 5662 83 9181 8688 26 9181 4847 88 9181 8387 30 9182 4732 88 9182 441 45 9182 3588 23 9182 9140 73 9183 1195 65 9184 7393 3 9185 2227 66 9186 1836 6 9186 4242 23 9188 1778 63 9189 7166 99 9189 5302 72 9189 5543 75 9191 4627 76 9192 383 77 9193 6892 86 9193 2348 1 9193 1426 43 9194 3777 24 9194 3799 58 9195 9775 96 9195 3738 68 9195 4258 14 9195 6439 25 9196 7667 43 9196 8692 41 9197 3335 96 9198 3840 47 9198 8828 13 9199 6250 9 9200 7976 16 9201 5587 33 9201 4120 33 9201 7221 41 9202 3906 11 9202 4343 99 9203 9828 67 9204 6739 18 9205 3249 30 9206 6076 58 9206 8020 39 9207 9464 31 9207 556 56 9209 8605 52 9209 6922 50 9209 4894 11 9210 9677 6 9210 4026 53 9211 8242 34 9212 666 97 9213 1895 60 9213 7847 1 9213 3662 74 9215 7439 54 9215 4670 1 9216 7755 56 9217 9059 28 9220 7773 15 9220 3133 54 9220 6066 42 9222 8601 90 9222 2338 11 9225 6578 74 9226 6813 90 9227 7007 63 9228 738 25 9228 5493 76 9229 8553 27 9230 6813 98 9230 1567 33 9231 5807 47 9232 5984 15 9232 8343 6 9233 5606 14 9234 9527 68 9234 3988 97 9234 3346 90 9236 933 70 9237 3649 42 9238 7124 63 9238 615 37 9239 4356 30 9239 3581 25 9240 2876 42 9240 5655 52 9241 5505 34 9241 9927 37 9241 6630 82 9241 9356 45 9242 685 64 9242 6085 30 9242 9326 42 9243 5548 12 9243 6905 91 9243 9619 32 9245 129 7 9246 8455 92 9246 6584 8 9246 8141 62 9248 1076 55 9250 2374 13 9250 8009 42 9251 6155 29 9251 6820 57 9252 6425 7 9253 7688 61 9254 4531 46 9254 3663 14 9254 4790 3 9255 4985 97 9256 2190 80 9256 6648 75 9257 1066 58 9257 4390 82 9258 140 24 9258 9130 48 9258 6580 88 9259 2075 95 9261 7218 94 9261 9508 95 9261 347 89 9262 8717 38 9263 9356 20 9263 6783 0 9264 5532 39 9264 276 54 9264 166 44 9265 1471 13 9265 8152 72 9266 5593 86 9266 7010 21 9266 4025 22 9266 8570 52 9267 76 19 9267 7140 79 9267 2572 36 9269 3487 13 9269 3050 65 9270 9416 44 9272 8727 55 9273 3807 53 9274 2120 36 9275 6883 90 9276 7413 88 9276 4362 92 9277 3107 55 9279 9585 13 9280 1315 93 9281 7330 46 9281 2033 66 9281 1960 70 9282 4732 16 9282 1885 18 9282 9231 53 9284 9714 36 9285 878 5 9287 2266 32 9287 5853 77 9288 9753 68 9288 4560 98 9289 1595 22 9289 5949 98 9290 9067 96 9290 4359 8 9291 3942 40 9291 2822 53 9294 8178 9 9294 6926 78 9294 5592 39 9294 3075 17 9295 4381 39 9295 9761 9 9295 8535 36 9296 3358 70 9296 6753 52 9296 7485 24 9298 5877 22 9298 7966 72 9298 7742 0 9300 8136 3 9300 4602 30 9303 6248 26 9304 7582 25 9305 1647 97 9305 1545 36 9305 2787 83 9306 4454 86 9306 1449 40 9306 3861 75 9308 2667 21 9309 4610 7 9309 4100 59 9309 5505 75 9310 9973 63 9310 1763 80 9311 6478 93 9312 4325 66 9313 3976 87 9314 2097 9 9314 3646 63 9314 9364 84 9314 9265 75 9315 8095 43 9315 6640 52 9315 9004 33 9316 1654 92 9316 2326 5 9316 3751 63 9316 7474 87 9317 318 76 9319 3751 20 9321 7356 15 9321 1860 21 9321 9387 69 9322 9956 97 9322 600 3 9322 4498 12 9322 7782 39 9323 953 55 9323 893 76 9324 5577 84 9325 9446 35 9326 1964 67 9326 3589 23 9326 4158 79 9326 5601 96 9326 4511 60 9326 9636 46 9327 6012 10 9327 5344 16 9327 7321 23 9327 9954 46 9328 7109 27 9329 4999 0 9330 5192 58 9334 6601 90 9334 9007 94 9336 3765 5 9338 9469 67 9341 8896 65 9343 5822 54 9343 5912 25 9343 4984 43 9343 2919 48 9344 5402 41 9344 5662 57 9346 5031 37 9346 5216 55 9346 3780 49 9347 7595 7 9347 644 24 9347 8931 37 9348 7779 83 9348 6968 18 9348 6867 62 9349 5421 87 9349 4355 100 9349 8104 39 9349 9096 81 9350 7624 56 9351 9718 57 9351 1944 75 9351 2276 5 9351 5006 24 9352 911 56 9353 9507 6 9355 9472 95 9355 6821 35 9355 8867 38 9356 1176 8 9357 2392 47 9357 6006 83 9357 1431 56 9358 7912 26 9359 5255 88 9359 4231 86 9361 594 27 9361 8733 35 9361 5890 49 9361 9358 45 9362 9468 50 9362 8121 60 9363 3290 13 9364 3429 74 9365 7462 31 9366 9426 24 9367 100 41 9367 6193 41 9367 941 96 9368 3665 0 9368 3598 24 9368 3878 18 9369 7932 23 9370 3959 2 9370 8649 4 9371 7477 65 9371 8032 82 9371 3433 99 9372 905 80 9372 213 85 9373 2521 84 9374 8598 27 9375 5109 3 9376 1176 5 9376 7464 12 9377 9081 5 9377 7839 8 9378 830 19 9378 88 85 9378 5826 83 9379 9661 71 9381 3041 35 9382 7799 93 9382 9662 69 9383 7339 32 9384 5608 19 9384 2342 42 9384 1182 1 9385 999 55 9385 6864 74 9386 9695 4 9387 9875 94 9388 800 72 9389 9330 31 9389 2912 43 9390 5655 31 9390 550 97 9390 2171 24 9391 4023 19 9395 8034 95 9395 256 62 9395 7955 71 9395 869 2 9396 5320 37 9397 2503 100 9398 6449 16 9398 284 27 9398 3291 73 9398 4158 54 9399 9384 72 9399 7248 81 9400 2991 66 9400 3914 88 9400 9516 60 9400 8429 59 9400 2037 72 9401 8248 82 9401 727 40 9402 1137 90 9403 6448 60 9404 5342 26 9404 4069 22 9405 3112 91 9405 3311 8 9405 1373 67 9405 4761 42 9408 3971 56 9408 8229 58 9409 2884 72 9409 2989 44 9410 88 88 9412 6244 63 9412 1044 90 9412 9807 88 9412 8033 6 9412 3057 25 9412 1824 79 9413 6886 49 9414 2899 24 9414 9329 73 9414 3914 70 9415 9265 65 9415 5495 92 9418 8253 66 9418 9582 87 9419 487 84 9419 7020 68 9420 621 69 9421 4887 38 9421 2206 10 9421 3531 23 9422 1244 40 9423 8577 4 9423 1451 0 9424 2328 70 9424 8831 91 9425 9388 61 9426 7294 11 9427 4220 80 9428 4029 32 9429 5194 11 9430 3488 94 9434 9384 100 9434 4828 31 9434 9837 92 9434 9781 81 9436 7816 91 9436 6094 59 9441 7177 98 9441 3168 9 9443 9370 62 9443 3310 93 9445 7384 91 9445 91 39 9445 5195 79 9448 4922 95 9448 6638 49 9449 9778 93 9449 3125 38 9450 4015 38 9450 464 46 9450 8169 56 9450 2089 1 9451 9704 30 9452 7714 15 9453 575 73 9453 9320 57 9455 4869 67 9456 8248 54 9456 145 57 9457 9087 25 9458 6241 99 9459 7441 13 9460 5902 39 9461 4929 79 9461 2813 2 9462 4500 84 9463 9747 85 9463 8063 23 9463 4762 72 9465 6489 94 9466 3043 6 9469 9795 65 9469 9208 51 9469 9026 43 9471 9447 42 9471 5876 39 9472 6712 4 9473 1452 24 9473 1201 11 9474 3879 10 9474 951 71 9474 9595 57 9474 6078 0 9475 3092 90 9476 8386 75 9476 5364 1 9476 5545 36 9477 973 48 9477 9726 93 9478 3413 9 9479 4679 95 9479 810 61 9479 4613 58 9480 2994 77 9482 23 98 9483 6474 63 9485 3936 38 9485 6717 31 9486 5749 71 9487 4984 73 9488 4026 8 9489 1065 27 9490 2695 45 9490 9146 25 9491 650 8 9492 2114 15 9492 536 47 9494 2698 37 9494 8106 0 9494 7542 12 9495 2363 61 9495 5622 19 9496 4433 43 9496 8861 57 9496 3676 74 9497 4396 1 9497 8910 83 9497 1789 64 9497 8789 50 9498 6048 81 9499 7875 97 9499 9541 81 9499 3266 34 9500 4401 60 9501 3531 52 9501 8736 40 9501 9054 22 9501 3222 80 9501 4215 100 9502 9612 82 9504 3104 9 9504 9920 33 9505 7192 8 9505 7140 22 9506 2606 2 9507 5164 48 9507 6880 85 9508 3309 86 9509 9153 9 9509 411 22 9509 7323 48 9510 8096 25 9511 7142 40 9514 1902 87 9514 5591 27 9514 3569 0 9515 5573 21 9516 4427 66 9516 4147 51 9518 692 83 9519 9911 53 9520 5269 21 9521 9813 88 9521 5856 1 9521 5685 21 9522 2650 32 9522 8533 94 9522 9543 62 9522 1668 5 9523 7602 39 9524 3322 40 9525 7313 80 9526 8307 93 9527 513 99 9527 3411 88 9527 3739 53 9527 5349 35 9528 7504 17 9529 6311 72 9531 3519 63 9531 9968 63 9532 5729 93 9532 7847 7 9533 6279 23 9533 7147 92 9534 6093 74 9535 7946 62 9536 5858 29 9538 8102 37 9539 5903 39 9539 5683 77 9539 7289 95 9540 9023 29 9541 6612 40 9541 3206 80 9542 7975 82 9544 5174 44 9545 563 20 9546 6855 62 9546 3999 45 9546 7973 11 9546 8958 94 9547 8909 14 9547 3808 57 9547 4014 40 9547 5683 63 9547 4116 66 9548 9712 70 9549 9885 64 9549 5500 17 9549 3447 17 9550 8696 34 9550 3600 86 9550 511 31 9550 2926 4 9551 1617 0 9551 8389 37 9551 5214 42 9552 8335 65 9552 7738 57 9553 973 3 9554 2584 56 9555 8989 76 9556 8142 80 9556 2308 88 9556 5231 85 9556 4286 79 9557 7883 70 9559 539 64 9559 952 1 9559 3737 75 9559 5550 94 9559 7367 36 9560 5814 20 9560 720 61 9560 6985 4 9561 7708 34 9561 6300 30 9561 6307 12 9561 5054 26 9562 8308 13 9562 7699 78 9563 5943 56 9564 4505 75 9564 6061 21 9566 3121 9 9566 3079 84 9568 6489 10 9568 1583 87 9569 9579 75 9569 1426 35 9569 4271 66 9570 2992 26 9571 9094 8 9571 3227 58 9571 5696 46 9572 7783 66 9572 7696 78 9573 5255 23 9573 5549 4 9574 2416 23 9574 2251 79 9574 8487 42 9574 2834 36 9576 575 7 9576 1019 100 9577 7627 95 9577 6536 53 9577 6737 5 9578 7586 22 9578 5612 18 9580 6935 66 9581 2441 4 9581 5288 45 9581 434 35 9583 9833 94 9584 5934 26 9585 2295 64 9585 7042 59 9586 6097 50 9586 5533 14 9587 4468 15 9587 7131 71 9588 3334 84 9588 5704 34 9589 779 92 9590 9183 36 9591 1234 43 9591 8729 95 9592 3718 14 9592 5428 31 9592 4563 89 9594 2854 75 9596 9838 52 9596 5132 16 9596 8344 1 9598 6179 26 9598 8585 2 9599 8712 54 9599 5251 35 9599 7972 28 9599 9200 4 9600 3943 43 9600 6075 34 9602 8822 77 9602 109 12 9602 4126 61 9603 9363 39 9603 8014 43 9606 5221 40 9606 1088 40 9607 8103 34 9607 1778 81 9608 2028 6 9608 4708 38 9608 4322 28 9609 5149 5 9610 3587 66 9610 1785 3 9611 2815 47 9611 8261 62 9611 2400 4 9611 8664 11 9613 7048 63 9613 2670 40 9614 8977 26 9615 9089 44 9615 3841 68 9616 6725 84 9616 9222 74 9617 7628 53 9618 8944 4 9618 5825 24 9619 147 60 9619 5702 22 9619 577 15 9620 6765 16 9621 1350 2 9621 4235 53 9621 3428 72 9622 1967 6 9622 8127 31 9623 5204 67 9623 6906 20 9624 829 65 9624 8511 96 9624 8960 80 9624 3509 35 9624 6663 42 9624 9896 89 9625 2120 82 9625 1354 34 9626 5853 77 9626 5237 49 9627 6144 84 9628 6464 64 9629 2574 90 9632 6436 34 9632 2779 88 9632 8826 93 9634 8571 52 9636 7553 31 9637 1986 22 9637 9758 62 9638 3676 0 9639 2950 16 9641 5476 77 9641 9499 97 9641 5216 91 9642 9082 3 9642 8720 18 9644 7319 20 9644 8039 54 9644 8462 32 9646 8518 25 9646 1555 91 9649 556 62 9650 776 59 9651 9784 40 9652 7592 26 9652 8518 70 9652 6623 94 9653 6024 39 9654 8905 80 9654 5152 52 9655 7643 6 9655 3127 83 9657 8235 51 9658 5698 80 9659 2700 65 9659 2335 47 9659 5722 67 9659 2123 85 9660 6916 50 9660 2491 56 9662 520 4 9663 1656 15 9664 6870 47 9664 7966 36 9665 1747 41 9665 2650 58 9665 3879 30 9666 7974 82 9667 965 77 9668 2293 95 9670 2449 50 9670 5447 74 9671 4848 64 9672 2434 57 9672 2673 0 9672 1680 38 9673 3825 98 9673 4670 62 9673 5473 40 9674 905 64 9674 233 68 9675 6887 83 9677 3638 21 9677 9913 5 9677 2531 95 9678 7525 66 9679 3235 78 9680 2102 0 9681 6663 36 9682 9032 43 9682 3910 90 9682 1167 52 9682 5120 21 9683 9057 29 9683 8571 81 9684 460 9 9684 2829 89 9684 4071 1 9684 4666 11 9685 9385 17 9685 4308 57 9685 1385 11 9685 8799 6 9686 1896 8 9686 9156 52 9687 6633 34 9688 5501 80 9690 3513 14 9690 2455 22 9691 4571 4 9692 4105 4 9692 3192 27 9692 221 31 9693 3082 5 9694 8409 11 9695 5466 39 9695 2533 9 9696 6105 53 9696 1858 51 9697 7892 92 9697 6865 75 9697 3931 90 9698 5100 94 9699 1680 56 9701 2400 2 9701 6892 42 9701 6734 70 9701 8081 9 9702 3296 38 9702 1269 51 9703 5441 2 9706 4456 27 9708 2870 73 9708 9853 98 9708 2021 24 9710 4316 55 9711 9434 88 9711 3725 69 9712 7406 77 9712 4235 90 9713 8080 75 9714 7035 83 9715 2119 86 9715 1577 24 9715 5740 9 9716 4775 45 9716 2781 55 9719 531 13 9719 3793 4 9720 3208 57 9720 8381 3 9720 8914 84 9720 4755 49 9723 1174 74 9723 4918 43 9723 1502 47 9725 1855 24 9725 2204 85 9726 1582 76 9726 11 75 9727 6638 5 9728 1968 24 9729 5545 82 9730 6050 71 9731 7644 94 9731 3197 69 9732 6513 44 9733 5840 47 9733 6410 61 9734 2504 63 9735 6453 85 9735 4672 100 9736 3908 26 9736 9042 58 9736 8452 71 9736 5641 23 9736 6351 61 9737 2374 53 9737 1920 17 9739 9533 53 9739 6546 28 9740 6043 23 9741 4805 64 9744 8087 90 9744 5177 35 9746 5004 42 9747 6969 81 9748 1356 87 9748 5153 46 9749 6585 82 9750 1474 81 9751 1616 95 9752 4004 57 9753 6473 5 9753 1094 41 9754 9829 72 9754 6396 76 9754 5178 24 9754 9610 47 9755 4218 17 9755 1691 38 9755 3326 57 9755 1670 85 9757 8632 43 9757 9146 61 9757 2302 31 9757 3289 86 9758 8772 99 9758 323 25 9758 6088 59 9759 1553 53 9760 547 49 9761 3079 44 9762 806 65 9762 163 20 9763 7866 1 9763 2529 77 9763 2215 47 9763 4821 94 9765 2331 77 9765 9059 39 9768 1886 52 9769 9393 62 9770 7999 89 9770 3022 77 9772 5982 44 9773 9063 11 9773 5663 81 9774 6830 3 9774 1195 40 9777 7401 43 9777 8869 39 9779 4734 24 9780 7799 19 9780 2500 92 9780 4983 36 9783 4414 82 9784 9661 33 9784 3219 8 9785 5876 36 9785 7479 68 9785 9797 87 9786 1340 98 9786 5459 9 9786 7989 8 9786 4968 56 9787 7844 65 9787 3692 24 9788 221 37 9789 2955 23 9790 4717 86 9791 7213 29 9792 3047 13 9792 7291 49 9795 8296 29 9795 8796 68 9796 8710 44 9796 1290 20 9796 6632 4 9797 7672 72 9798 2481 57 9799 9108 35 9800 5864 60 9801 9796 21 9803 9096 72 9804 6809 15 9804 7404 3 9806 2441 57 9806 945 53 9807 9733 7 9807 3942 19 9807 9474 28 9807 3715 61 9808 8726 63 9808 8703 64 9811 7068 21 9812 8211 40 9812 903 8 9812 4311 63 9812 4656 22 9813 9621 37 9813 4200 35 9814 8907 94 9814 2977 34 9816 1278 6 9817 1706 76 9818 4840 33 9818 4815 39 9819 1231 40 9820 2168 65 9821 7909 80 9821 1811 96 9822 7554 80 9824 6602 99 9825 5102 47 9825 7201 13 9826 2481 74 9826 6887 91 9826 4452 75 9827 9913 62 9827 431 8 9828 8061 76 9828 5676 98 9829 5771 54 9829 9414 69 9831 5335 88 9831 3700 38 9832 4289 19 9832 620 46 9833 9683 81 9835 8066 58 9837 810 63 9837 3418 100 9837 6679 83 9838 9612 18 9840 1380 81 9840 3439 27 9843 1695 44 9844 4468 10 9845 9580 48 9845 5482 19 9845 4310 43 9845 8721 32 9846 4537 93 9846 71 78 9848 7933 40 9848 1126 19 9851 6291 95 9851 9398 70 9852 4408 72 9853 9103 23 9853 8152 22 9853 8702 15 9854 9299 66 9854 7664 16 9854 9056 98 9854 710 63 9856 5663 29 9856 5891 98 9857 2486 36 9857 4831 95 9858 621 33 9859 8165 49 9860 9055 96 9860 6986 27 9861 3758 85 9862 9542 89 9862 3464 9 9863 5959 53 9864 5658 48 9864 797 87 9864 5245 29 9864 939 47 9865 6433 44 9868 5146 43 9870 6642 38 9871 9746 34 9871 5650 48 9872 6844 89 9873 7362 95 9873 6483 7 9874 5238 20 9876 8050 1 9876 8644 85 9877 8953 86 9877 2607 83 9877 9326 94 9877 7648 50 9878 5664 65 9878 5725 30 9878 9397 74 9880 6281 26 9880 1230 81 9880 4359 73 9880 6221 87 9881 569 100 9881 1655 82 9882 8553 11 9882 8786 58 9883 524 22 9884 4630 53 9886 780 31 9887 4260 10 9887 9487 75 9888 5850 12 9889 644 7 9890 5411 77 9891 3661 99 9891 9000 85 9892 8244 13 9892 7287 68 9893 5990 52 9894 7981 3 9896 8328 34 9896 757 31 9898 4551 76 9899 1995 11 9899 8511 7 9900 7425 47 9900 7946 70 9901 4786 32 9901 1217 37 9902 9359 97 9902 5184 59 9903 4451 94 9904 17 73 9904 1387 90 9904 2512 86 9905 7686 82 9906 7517 62 9908 3862 61 9908 7638 48 9908 8659 74 9908 1953 100 9909 8903 89 9909 5414 68 9910 1515 98 9911 8218 25 9912 232 61 9912 268 89 9912 4640 98 9912 6588 71 9914 9394 56 9915 5259 93 9915 983 5 9915 7306 72 9918 9533 20 9919 1547 75 9919 5462 3 9920 2450 73 9920 7602 49 9920 6075 89 9922 513 7 9922 1754 17 9923 5228 52 9923 798 57 9923 9147 85 9924 4053 89 9925 9370 92 9925 4765 9 9925 8002 86 9925 5111 27 9927 6951 57 9927 4922 29 9927 1631 83 9927 593 86 9927 9490 91 9928 8192 75 9929 2101 81 9929 9313 51 9930 7284 55 9931 6850 33 9932 9050 11 9933 1257 40 9934 3182 47 9935 958 59 9936 3138 23 9936 699 83 9937 701 27 9937 7652 35 9937 3945 96 9938 7368 93 9939 5181 42 9939 1277 74 9939 8754 66 9941 6925 63 9941 782 81 9942 2676 33 9942 8579 71 9942 8486 4 9943 5731 84 9943 5703 19 9943 7327 53 9944 8425 20 9945 6278 14 9945 9285 79 9945 9740 80 9945 2636 74 9946 5029 75 9947 2163 97 9949 9258 80 9949 9400 29 9951 1742 63 9952 2087 84 9952 7225 80 9954 7964 45 9955 8491 60 9955 6319 69 9955 784 58 9955 8010 47 9957 8897 92 9957 9458 37 9957 2186 86 9957 6808 12 9958 9662 29 9958 8125 32 9960 9020 2 9960 2331 93 9961 7228 64 9961 5492 82 9961 6291 77 9961 8414 68 9962 6554 19 9962 2470 67 9965 5088 80 9965 1588 72 9965 4115 72 9967 5482 2 9967 1956 95 9967 8982 76 9969 3661 30 9969 9812 68 9970 5200 40 9970 1089 89 9970 3765 8 9971 5709 60 9971 3528 82 9971 8484 15 9972 562 9 9972 3701 7 9973 6182 18 9973 8402 24 9973 9509 71 9975 765 63 9975 7182 13 9975 7588 58 9975 806 43 9976 6665 52 9977 5581 21 9977 3719 20 9978 2460 5 9978 5080 40 9978 936 30 9979 6096 31 9980 2906 58 9980 5349 96 9980 3626 98 9981 3688 63 9982 2144 89 9982 5772 82 9982 6351 39 9983 612 76 9983 4885 73 9983 4147 21 9983 4846 93 9983 1814 85 9983 4866 41 9984 8691 33 9984 5015 13 9985 267 12 9986 1690 8 9986 4580 60 9987 5448 93 9988 1330 3 9989 1255 42 9989 6820 45 9990 4732 20 9990 4622 62 9991 2183 29 9991 9894 43 9991 9942 44 9992 8037 47 9992 4769 84 9992 8914 55 9992 7429 100 9992 2562 2 9993 2368 15 9993 7141 35 9994 9022 32 9995 9327 78 9995 6484 50 9996 6911 23 9997 8699 10 9997 8746 95 9997 5602 9 9998 1768 34 9998 1711 42 9999 1392 91 9999 3411 12 9999 6517 76 9999 468 0 pyTooling-8.11.0/tests/data/Graph/EdgeLists/graph_n1000_m1500_dir_w0_100.edgelist000066400000000000000000000372401513317154500267060ustar00rootroot000000000000000 188 5 0 917 97 0 792 81 1 906 80 2 693 99 3 461 49 4 296 2 5 334 9 5 688 48 5 638 16 5 693 11 5 94 62 6 99 52 6 47 14 6 751 81 7 455 46 8 157 31 8 260 17 10 237 28 11 736 79 12 651 41 12 78 88 13 941 0 13 332 39 13 445 21 14 531 32 14 146 68 15 969 13 15 403 95 16 365 58 16 821 52 17 325 7 19 433 95 23 492 43 23 121 98 23 372 61 23 207 48 25 649 89 25 739 80 26 524 60 27 239 42 30 659 13 30 162 32 30 53 73 31 865 12 33 707 24 33 259 86 34 521 44 34 900 12 34 978 90 35 544 7 36 523 38 36 8 25 36 5 80 38 358 30 39 646 85 40 143 34 40 243 67 40 79 58 41 183 53 41 818 10 42 171 57 42 375 26 43 462 2 44 204 20 44 781 57 45 144 30 48 344 50 49 442 89 49 140 14 50 842 10 51 648 0 52 805 35 53 739 79 53 110 14 53 746 62 54 156 30 54 735 37 57 722 40 58 955 60 58 900 10 60 62 52 61 110 4 61 247 92 62 470 0 63 979 31 63 870 0 64 885 90 65 102 24 66 621 70 66 870 48 66 152 23 67 620 31 68 882 93 68 773 63 69 834 58 71 809 67 73 134 74 74 980 10 76 957 19 76 990 67 76 488 16 76 569 65 76 331 5 76 921 96 77 900 41 78 393 73 78 713 48 78 108 77 78 35 51 79 582 60 79 497 7 80 400 25 80 116 27 81 978 7 81 678 7 81 749 71 82 596 43 82 255 18 82 750 16 83 996 65 84 502 68 85 622 44 85 948 80 86 684 9 86 304 79 87 459 99 88 96 49 88 650 14 89 603 64 89 858 10 89 445 41 90 70 100 90 892 24 90 801 82 91 396 26 91 550 43 91 676 72 92 220 8 93 143 100 93 551 73 93 570 52 94 900 13 94 358 0 94 271 36 94 747 27 95 93 61 96 945 5 97 910 36 98 735 2 98 161 84 99 776 95 99 339 70 100 766 46 100 982 55 101 180 79 101 778 94 102 193 50 102 504 96 103 476 10 103 776 26 103 821 79 104 945 49 104 539 76 105 570 5 105 234 56 105 668 19 106 925 79 108 701 59 108 176 49 108 42 13 109 792 54 109 316 27 110 662 56 110 822 3 111 67 89 111 265 48 112 763 19 112 10 29 112 951 38 113 394 25 114 83 31 114 797 74 116 674 23 116 326 63 117 702 30 117 852 64 118 349 74 118 758 66 119 861 62 119 382 87 121 663 26 122 201 35 122 277 70 122 796 22 123 105 52 123 736 83 123 675 90 124 951 100 124 284 17 125 774 83 125 393 75 126 751 73 126 672 5 127 64 7 128 866 43 129 203 96 130 915 78 130 316 73 131 496 65 132 873 80 132 579 55 134 208 73 134 641 90 135 213 0 136 334 50 137 39 76 138 235 16 138 377 87 140 621 3 141 280 5 142 194 81 143 533 5 143 145 49 143 249 31 143 912 77 143 771 90 143 550 38 144 15 38 144 91 6 144 952 11 145 72 63 145 722 40 146 867 43 146 668 48 147 158 87 149 849 46 149 64 58 151 133 71 152 410 39 153 565 27 153 536 15 153 152 29 153 739 60 154 596 61 154 669 100 155 247 55 155 253 93 155 899 20 155 22 82 156 106 62 158 554 41 159 755 49 159 658 22 159 565 12 159 187 16 160 261 14 161 639 82 161 700 85 162 922 98 163 783 5 163 38 73 163 220 47 163 299 79 164 761 1 165 901 46 167 636 46 167 248 84 168 650 36 168 610 25 169 275 2 169 895 69 169 10 54 170 476 15 171 509 61 171 100 0 171 753 3 173 372 75 174 572 14 175 855 79 175 422 4 175 700 82 176 886 8 179 377 69 179 754 46 181 809 86 181 455 56 182 848 65 183 213 73 184 300 84 184 589 56 184 301 53 185 426 69 186 182 47 187 820 6 187 470 44 188 548 21 190 152 91 190 299 80 191 422 36 192 973 53 192 784 60 194 563 90 194 196 68 194 574 8 194 29 75 194 164 22 195 434 12 195 825 25 196 388 34 199 153 9 199 682 31 199 887 21 199 151 28 200 233 42 202 554 89 202 771 25 203 361 78 205 280 70 205 700 23 206 868 64 208 188 90 208 125 90 210 270 32 210 910 13 210 213 12 211 341 71 212 797 76 212 908 75 212 812 9 212 922 62 212 896 16 212 314 71 213 411 6 213 647 54 215 166 10 215 819 85 216 643 82 217 769 44 217 948 40 218 625 48 218 334 16 218 958 1 219 711 23 219 871 93 219 675 81 220 45 53 220 723 64 220 440 70 221 242 35 222 113 10 222 563 79 225 38 52 225 617 0 226 480 15 227 922 25 227 542 69 227 972 96 228 521 68 228 786 35 228 113 55 229 557 41 229 835 95 229 829 80 230 111 0 230 231 31 231 329 94 231 816 70 232 219 76 232 472 46 232 415 3 233 789 70 234 616 89 234 134 62 235 547 7 235 969 67 237 762 11 240 558 93 241 460 61 241 826 50 241 189 19 242 38 18 243 601 48 243 313 50 243 180 42 243 836 83 244 748 21 245 15 100 245 600 26 245 620 59 246 203 39 246 189 59 247 550 27 249 87 52 249 755 7 256 842 74 257 36 22 257 956 93 258 310 60 258 92 59 259 271 19 261 902 36 261 792 92 262 313 75 262 129 28 264 59 22 264 577 55 264 299 5 264 921 55 264 878 85 265 911 18 265 639 52 266 963 87 267 825 19 267 218 69 267 16 52 267 673 28 269 928 91 269 781 34 269 828 40 270 636 40 271 836 5 272 867 21 272 80 98 273 928 59 274 427 54 274 506 27 274 673 1 275 221 37 276 641 63 278 611 80 279 589 16 279 272 94 281 838 34 282 394 98 282 330 60 282 94 38 282 777 12 282 462 100 283 158 54 283 379 89 284 894 43 285 683 89 285 778 6 285 71 26 285 445 65 286 341 96 287 203 8 287 389 52 288 136 66 288 150 56 288 351 91 289 150 27 290 449 88 291 278 95 291 400 72 291 622 98 291 690 31 293 821 94 294 754 1 295 703 48 295 207 25 296 185 5 296 984 5 297 895 75 298 529 57 298 555 98 298 186 0 299 634 23 299 538 10 300 919 80 300 509 79 301 570 9 304 624 17 305 660 47 306 987 74 307 267 4 307 787 82 307 732 85 307 157 69 307 395 59 308 138 100 308 927 77 308 999 1 309 526 7 309 388 39 309 248 33 309 730 97 310 987 13 311 303 54 311 222 57 312 127 16 312 275 53 312 232 74 313 807 51 313 967 6 315 343 90 316 887 52 317 4 78 317 720 10 319 24 8 319 934 4 320 238 86 320 133 49 320 93 18 321 997 21 321 218 31 321 236 81 321 627 86 322 740 37 322 134 80 322 393 9 322 817 17 323 854 87 323 999 85 323 898 76 324 276 65 325 408 0 325 274 14 326 445 52 327 550 96 327 671 76 327 764 70 328 133 36 333 145 96 333 591 37 334 152 6 334 580 2 334 597 56 336 311 71 337 572 77 337 832 43 337 2 97 337 845 22 337 302 91 338 756 97 338 655 88 338 253 52 339 523 13 339 875 27 340 910 29 340 470 61 340 513 1 340 420 41 342 387 86 343 44 31 343 250 60 345 760 53 346 602 27 346 639 20 347 875 25 347 637 89 348 828 98 349 59 48 349 417 76 350 564 28 352 630 48 353 397 10 353 462 9 354 92 98 355 305 64 355 621 69 355 612 41 355 633 66 356 392 39 357 922 30 358 940 36 360 87 70 360 169 77 360 622 52 361 312 17 361 282 12 362 497 91 363 885 23 364 607 23 365 215 83 366 355 55 366 671 13 367 58 30 367 528 49 367 765 55 367 915 38 368 970 60 368 971 91 368 524 11 368 851 84 369 813 31 370 741 45 372 542 4 374 850 84 374 556 15 376 136 66 376 806 22 378 849 7 378 152 73 379 171 9 379 503 46 380 415 47 382 968 90 382 912 68 383 455 28 386 255 47 387 811 22 387 75 22 387 817 28 388 829 47 388 603 54 388 741 49 389 24 25 390 728 1 391 418 24 391 138 68 392 955 46 392 538 98 393 660 2 394 691 10 394 353 9 395 794 9 397 634 1 397 91 29 397 170 41 398 402 75 398 953 3 398 396 75 398 550 61 399 59 14 399 321 51 399 889 23 399 61 32 400 697 2 401 776 27 404 719 71 405 651 61 405 142 74 405 144 17 406 980 80 406 910 89 407 706 1 408 628 76 408 940 8 409 381 59 412 680 21 413 48 53 414 629 23 414 573 54 414 285 26 414 570 24 415 140 68 416 964 71 417 846 80 418 540 25 418 507 52 420 687 92 421 108 36 421 234 28 421 348 98 422 502 4 422 171 2 422 893 61 423 901 45 424 481 78 425 482 64 426 453 42 427 254 89 428 541 98 428 563 91 429 632 69 429 579 100 429 885 46 430 371 72 430 686 47 430 441 58 431 364 35 431 28 66 431 599 79 431 551 22 432 867 7 433 672 57 434 670 4 434 402 65 435 602 74 436 3 23 436 299 8 437 614 28 439 576 47 440 779 35 441 80 31 442 621 42 442 583 18 442 496 81 444 432 39 444 170 85 444 889 84 446 626 11 446 915 99 446 26 53 449 822 11 449 4 12 449 707 68 449 347 51 450 380 87 450 763 61 452 987 11 453 364 100 454 697 44 455 160 41 456 320 72 456 492 24 456 252 77 457 365 92 457 114 13 457 789 96 458 346 32 459 221 88 459 289 35 461 130 34 462 701 23 463 851 0 464 149 59 464 538 70 468 403 54 468 62 40 468 69 51 470 638 15 474 878 63 474 692 50 475 31 39 476 85 13 476 903 18 477 378 91 477 585 49 477 958 18 477 822 56 477 292 60 477 336 67 479 326 20 480 565 26 480 228 6 480 543 43 481 659 36 485 755 16 486 658 16 486 413 73 486 542 13 487 504 32 487 399 1 487 89 68 488 424 60 488 126 60 489 438 7 489 671 28 489 623 54 490 392 10 490 473 42 491 161 11 492 840 36 492 326 26 493 553 21 493 383 65 497 312 50 497 726 2 498 763 25 498 880 18 499 531 44 500 971 37 500 930 56 501 182 3 501 561 23 502 354 80 502 797 48 503 938 65 504 33 92 505 280 68 506 27 26 506 725 60 506 805 5 507 155 60 507 408 25 508 361 80 508 4 74 510 281 6 511 727 36 511 781 2 512 996 42 513 313 94 513 689 79 513 534 9 514 425 85 514 327 23 514 732 4 515 123 20 515 916 65 516 176 89 517 786 42 517 593 54 518 361 96 518 796 90 518 98 64 518 795 78 519 304 100 520 461 85 522 225 99 523 122 67 525 719 23 525 333 19 526 202 79 527 155 0 527 658 33 530 578 28 530 447 13 531 244 21 531 798 22 532 20 72 532 675 33 532 302 65 532 189 6 534 742 32 534 855 29 535 825 38 536 699 48 537 514 23 537 280 42 539 877 90 539 824 54 540 708 1 540 898 59 540 517 84 541 565 54 541 308 55 543 256 87 544 136 38 545 71 35 546 260 15 547 349 73 547 692 21 549 806 44 549 225 88 550 313 94 550 476 27 550 456 56 551 391 30 552 829 2 552 900 57 552 934 36 552 904 86 552 521 27 553 649 35 554 624 46 554 986 11 554 586 27 556 338 12 558 55 75 558 709 17 560 547 96 560 57 77 562 121 10 563 479 90 564 155 65 566 876 99 566 98 42 567 1 10 568 373 0 569 698 23 570 790 27 571 105 14 571 646 40 571 999 37 571 811 88 572 520 40 572 456 98 573 473 67 574 317 59 575 64 83 576 777 73 576 937 9 577 447 89 577 262 61 580 830 37 580 558 16 580 78 69 580 720 18 582 817 84 582 805 42 583 53 97 584 255 12 585 919 10 585 561 7 586 271 2 586 496 77 587 391 66 588 559 88 589 668 80 589 93 23 589 989 74 591 529 26 591 885 17 591 910 79 591 598 52 591 252 29 591 12 10 592 905 30 593 11 77 593 562 83 593 61 68 594 242 29 594 813 63 594 899 3 595 392 65 596 975 86 596 824 60 596 806 48 597 488 36 598 734 71 598 419 52 598 828 40 600 231 85 602 609 41 606 997 1 607 35 99 608 324 32 608 645 29 609 794 93 609 439 13 609 657 63 609 667 55 610 129 15 612 138 33 612 693 92 612 501 23 613 536 21 613 845 12 613 446 75 614 97 66 616 129 13 616 315 22 616 86 27 617 448 39 618 982 76 618 620 86 618 948 25 619 104 23 620 209 21 621 649 44 621 668 19 622 14 84 622 755 23 623 221 45 623 896 54 623 167 82 623 175 88 623 832 91 624 371 68 624 790 9 624 601 7 625 759 76 625 443 82 627 705 58 628 598 17 628 557 20 629 10 21 629 683 90 630 307 60 631 422 90 632 606 74 632 784 19 633 308 14 633 302 82 633 624 87 634 483 82 634 716 100 637 929 20 638 141 49 639 464 33 639 870 74 640 78 33 640 560 59 641 451 99 641 996 5 641 31 3 641 555 80 642 120 48 642 941 86 643 291 26 643 633 5 644 838 53 644 472 61 645 515 84 645 246 59 646 723 75 646 986 42 647 574 57 647 904 56 648 522 72 649 350 28 650 562 24 650 281 79 651 505 30 653 683 44 654 896 25 654 710 84 654 552 96 655 895 90 655 574 5 655 61 10 656 737 77 656 568 43 657 929 28 658 957 53 658 315 60 658 873 30 659 315 95 659 309 90 660 446 62 661 629 7 662 899 13 663 268 35 663 518 52 665 531 39 665 154 67 665 856 45 666 538 58 666 510 73 666 277 45 668 519 76 668 667 89 669 80 42 669 889 40 669 174 22 670 193 74 671 649 85 673 94 83 673 285 12 673 865 61 674 346 70 675 649 44 675 888 33 675 15 97 676 814 73 676 491 73 677 81 34 678 429 11 679 189 83 679 915 8 680 932 95 681 651 89 681 380 86 682 697 16 682 305 67 682 838 83 685 498 45 685 690 87 686 383 66 686 631 81 686 542 76 686 496 7 687 696 85 687 477 78 687 638 32 687 449 74 687 311 47 689 20 19 691 41 20 691 893 9 691 194 15 692 769 15 694 768 9 694 774 11 696 968 25 696 352 35 696 948 72 697 531 12 697 857 84 697 300 0 697 909 13 698 679 87 699 117 66 700 812 29 700 427 99 702 733 9 702 140 62 703 130 100 703 314 75 704 884 13 704 114 12 704 238 40 705 961 5 706 994 72 706 708 7 707 25 98 708 263 27 708 32 53 709 973 17 709 751 65 709 400 88 710 670 97 710 216 30 710 205 79 710 903 71 710 505 93 712 447 8 714 436 38 714 956 93 715 728 38 715 322 48 717 811 69 717 191 37 717 511 71 719 971 18 719 125 74 719 506 76 719 368 76 719 985 64 721 758 56 723 913 13 723 254 53 725 547 64 725 880 47 726 366 49 727 934 38 727 253 63 730 303 56 731 212 44 731 683 99 732 284 95 733 26 27 733 656 78 734 61 86 736 303 35 737 548 66 737 780 16 737 821 73 739 940 85 739 345 27 739 662 26 740 507 37 740 536 45 741 494 18 742 536 44 742 885 85 744 771 48 746 371 59 751 801 72 751 912 16 752 281 66 753 265 93 753 358 91 753 791 10 753 741 87 754 893 15 756 871 92 757 121 83 757 912 74 758 458 55 761 516 91 762 584 17 763 535 65 763 255 72 763 337 23 764 880 9 764 170 21 765 105 13 766 362 62 766 183 83 766 551 14 766 984 86 767 809 21 769 6 69 770 180 89 770 20 50 771 239 45 771 362 15 772 378 59 773 938 39 773 172 0 773 752 85 774 283 86 775 214 89 778 305 12 778 644 83 779 303 41 780 110 21 781 570 33 781 993 84 782 841 32 783 729 73 784 799 87 784 565 7 784 338 100 784 973 47 785 957 21 786 334 33 786 932 36 787 819 71 787 141 7 787 638 38 787 308 4 789 371 30 790 519 5 791 19 41 791 20 53 792 195 25 792 833 28 792 359 3 793 406 81 793 346 98 793 914 93 794 852 11 794 894 99 794 719 86 794 734 6 795 379 84 796 567 52 797 562 64 797 927 32 798 459 2 798 872 49 798 646 51 798 451 41 799 687 73 799 156 97 799 453 93 800 426 8 800 620 54 801 479 52 801 130 8 801 537 79 802 277 38 802 641 77 802 119 2 803 537 0 804 999 86 805 777 0 805 2 62 805 553 59 806 159 25 809 123 13 809 33 59 809 179 26 809 231 16 810 790 32 811 694 17 811 955 34 811 88 86 811 360 13 811 847 88 812 103 2 812 464 78 814 89 18 815 26 59 816 759 50 817 384 57 817 836 16 818 799 65 821 951 16 821 282 86 821 357 50 821 984 71 821 14 55 823 575 9 823 280 14 823 535 72 826 249 24 826 346 93 826 761 1 827 620 43 827 678 68 827 344 79 828 774 7 828 181 4 828 838 24 828 192 68 829 605 84 830 663 51 830 19 36 831 800 37 831 978 100 831 93 50 832 838 100 832 790 52 833 6 96 834 932 89 835 446 37 835 677 92 836 467 68 836 940 37 836 359 30 837 185 78 837 857 50 838 361 49 838 287 37 839 195 81 841 307 41 842 966 70 842 791 79 843 575 99 843 644 72 845 541 17 847 903 57 848 335 26 848 838 27 848 942 17 848 77 63 849 858 38 851 491 18 853 182 65 855 250 86 855 161 27 855 452 62 855 742 14 856 785 62 856 281 51 856 716 17 857 215 93 859 396 100 859 969 46 859 916 92 862 820 33 862 111 69 862 536 89 863 330 67 863 746 1 864 411 53 864 173 86 865 342 58 865 587 19 867 341 24 867 784 77 868 174 95 870 864 1 871 984 77 871 560 25 871 305 5 871 71 14 871 379 53 872 944 1 872 138 39 874 345 15 875 257 63 875 405 84 878 876 8 878 99 64 880 602 87 880 82 99 881 701 14 882 97 16 883 343 72 884 594 22 884 214 16 885 434 3 885 205 36 887 137 37 887 325 54 887 10 86 888 374 40 889 859 79 889 24 2 889 537 12 892 124 58 892 293 40 893 896 25 893 584 43 894 209 63 895 947 67 896 18 55 896 953 42 896 280 77 897 233 70 897 427 28 897 161 8 899 82 19 899 969 42 900 525 67 900 21 98 901 361 99 903 516 43 903 801 58 903 274 79 904 859 25 904 278 44 904 476 17 904 373 21 905 234 86 905 207 23 906 474 12 907 721 29 907 950 94 908 838 75 908 202 26 908 455 31 909 468 52 909 362 36 910 936 28 910 62 82 910 247 64 911 232 44 911 899 30 913 410 90 914 117 72 914 89 67 917 631 28 917 704 37 918 947 84 918 400 23 919 632 24 920 571 80 922 929 7 922 986 91 923 640 68 924 984 0 926 874 93 927 413 4 928 437 86 928 485 21 931 725 41 932 872 95 932 624 14 933 117 70 935 361 24 935 341 14 935 318 29 936 860 80 938 419 34 939 933 95 939 710 5 942 974 50 942 771 87 942 875 28 942 212 97 943 655 54 945 800 48 945 702 68 946 892 75 948 556 80 949 97 60 952 123 84 952 463 8 955 696 20 956 157 38 957 799 83 958 688 0 959 244 35 959 629 65 959 5 57 960 34 77 960 909 79 960 701 8 961 989 78 963 477 27 964 389 59 964 138 77 964 7 61 965 558 76 965 998 4 966 408 51 966 78 13 967 457 54 969 32 3 969 700 87 969 0 20 971 209 36 972 487 2 973 288 95 973 476 45 973 318 17 973 961 27 973 991 8 973 835 72 974 92 47 974 958 84 975 596 91 975 741 12 976 133 95 976 353 78 978 119 3 979 266 27 980 688 83 981 101 47 983 844 96 983 304 60 984 768 59 985 268 93 985 483 31 985 455 92 986 903 46 987 845 46 987 687 47 987 430 99 990 65 85 991 324 73 991 398 39 994 163 95 994 197 60 995 83 55 995 401 9 995 949 83 996 754 90 997 420 6 997 402 98 998 265 83 999 36 25 pyTooling-8.11.0/tests/data/Graph/EdgeLists/graph_n100_m150_dir_w0_100.edgelist000066400000000000000000000024371513317154500265460ustar00rootroot000000000000000 4 93 0 93 43 0 58 91 1 16 51 2 25 68 2 69 47 2 78 8 4 41 59 4 74 71 5 83 40 5 26 19 6 3 68 7 96 34 7 89 82 7 47 91 8 58 98 9 82 94 10 71 20 10 48 56 11 87 66 11 65 43 13 62 75 13 24 73 14 58 61 14 26 9 15 48 11 15 46 53 15 96 88 16 39 73 18 89 47 19 83 7 19 71 2 21 77 20 21 59 75 21 48 27 23 48 68 25 2 9 25 55 87 25 74 93 26 6 81 27 5 72 27 73 13 27 75 23 29 70 0 30 97 84 30 71 98 31 76 65 33 57 59 33 38 40 34 29 47 34 39 78 34 21 13 34 45 46 35 50 25 35 55 48 38 51 42 38 71 20 39 20 81 40 33 94 40 65 19 41 60 96 41 64 1 41 76 68 42 1 93 43 73 21 43 87 79 43 32 0 45 61 40 45 27 97 45 58 37 47 60 62 47 75 28 47 22 36 49 78 52 49 71 9 50 22 100 50 9 61 51 91 35 52 82 24 52 87 20 52 22 80 53 75 49 53 13 68 54 90 91 55 67 48 56 44 61 57 21 97 58 56 37 59 20 44 59 60 23 60 32 45 60 84 57 60 19 42 60 52 3 61 34 63 61 64 10 61 11 11 61 81 51 62 97 76 62 45 41 63 42 7 64 69 17 65 28 78 66 55 11 66 64 100 67 20 89 68 78 12 69 94 98 69 86 70 70 36 37 71 89 25 72 88 69 72 11 13 74 96 48 75 54 33 76 93 87 76 43 58 77 64 88 78 53 32 78 99 3 78 61 13 80 21 13 81 86 11 82 93 72 85 99 65 85 17 27 86 66 13 87 26 53 87 50 58 88 65 42 88 45 4 88 28 85 89 65 38 90 73 2 91 23 39 91 69 0 92 21 89 92 25 93 93 42 39 94 69 5 94 47 38 94 80 78 95 35 18 96 91 33 96 52 75 98 52 45 98 93 39 98 69 19 99 33 97 99 28 0 pyTooling-8.11.0/tests/data/Requirements/000077500000000000000000000000001513317154500203305ustar00rootroot00000000000000pyTooling-8.11.0/tests/data/Requirements/requirements.Common.txt000066400000000000000000000000261513317154500250410ustar00rootroot00000000000000ruamel.yaml ~= 0.18.6 pyTooling-8.11.0/tests/data/Requirements/requirements.Documentation.txt000066400000000000000000000000521513317154500264210ustar00rootroot00000000000000-r requirements.Common.txt sphinx ~= 8.1 pyTooling-8.11.0/tests/data/Requirements/requirements.Git.txt000066400000000000000000000001751513317154500243410ustar00rootroot00000000000000# Protocol + Git-URL @ Ref git+https://github.com/pyTooling/pyTooling.git git+https://github.com/pyTooling/pyTooling.git@dev pyTooling-8.11.0/tests/data/Requirements/requirements.HTTPS-ZIP.txt000066400000000000000000000001161513317154500251530ustar00rootroot00000000000000# URL_to_ZIP # Package https://github.com/ghdl/ghdl/archive/master.zip#pyGHDL pyTooling-8.11.0/tests/data/Requirements/requirements.txt000066400000000000000000000001301513317154500236060ustar00rootroot00000000000000-r requirements.Documentation.txt -r requirements.Git.txt -r requirements.HTTPS-ZIP.txt pyTooling-8.11.0/tests/performance/000077500000000000000000000000001513317154500172355ustar00rootroot00000000000000pyTooling-8.11.0/tests/performance/Graph/000077500000000000000000000000001513317154500202765ustar00rootroot00000000000000pyTooling-8.11.0/tests/performance/Graph/Graph.py000066400000000000000000000162021513317154500217120ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.Graph.""" from pathlib import Path from pyTooling.Graph import Graph as pt_Graph, Vertex as pt_Vertex, DestinationNotReachable from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class EdgeLinking(PerformanceTest): def test_LinkNewVertex_Flat(self) -> None: def wrapper(count: int): def func(): rootVertex = pt_Vertex(0) for i in range(1, count): rootVertex.EdgeToNewVertex(i) return func self.runSizedTests(wrapper, self.counts) def test_LinkNewVertex_Linear(self) -> None: def wrapper(count: int): def func(): vertex = pt_Vertex(0) for i in range(1, count): vertex = vertex.EdgeToNewVertex(i).Destination return func self.runSizedTests(wrapper, self.counts) class RandomGraph(PerformanceTest): def ConstructGraphFromEdgeListFile(self, file: Path, vertexCount: int) -> pt_Graph: graph = pt_Graph(name=str(vertexCount)) vList = [pt_Vertex(vertexID=v, graph=graph) for v in range(vertexCount)] with file.open("r", encoding="utf-8") as f: for line in f.readlines(): v, u, w = line.split(" ") vList[int(v)].EdgeToVertex(vList[int(u)], edgeWeight=int(w)) # lenBFS = [] # # lenDFS = [] # for v in vList: # bfsList = [u for u in v.IterateVerticesBFS()] # # dfsList = [u for u in v.IterateVerticesDFS()] # lenBFS.append(len(bfsList)) # # lenDFS.append(len(dfsList)) # # print(f"{v}: bfs={len(bfsList)}; dfs={len(dfsList)}") # print(f"BFS: min={min(lenBFS)} avg={mean(lenBFS)} max={max(lenBFS)}({lenBFS.index(max(lenBFS))})") # # print(f"DFS: min={min(lenDFS)} avg={mean(lenDFS)} max={max(lenDFS)}({lenDFS.index(max(lenDFS))})") return graph def test_BFS(self) -> None: def wrapper(graph: pt_Graph, componentStartVertex: int, componentSize: int): def func(): rootVertex = graph._verticesWithID[componentStartVertex] bfsList = [v for v in rootVertex.IterateVerticesBFS()] self.assertEqual(componentSize, len(bfsList)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_DFS(self) -> None: def wrapper(graph: pt_Graph, componentStartVertex: int, componentSize: int): def func(): rootVertex = graph._verticesWithID[componentStartVertex] bfsList = [v for v in rootVertex.IterateVerticesDFS()] self.assertEqual(componentSize, len(bfsList)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_ShortestPathByHops(self) -> None: def wrapper(graph: pt_Graph, componentStartVertex: int, componentSize: int): def func(): startVertex = graph._verticesWithID[49] destinationVertex = graph._verticesWithID[20] try: vertexPath = [v for v in startVertex.ShortestPathToByHops(destinationVertex)] except DestinationNotReachable: pass # print(f"path length: {len(vertexPath)}") # self.assertEqual(6, len(vertexPath)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_ShortestPathByWeight(self) -> None: def wrapper(graph: pt_Graph, componentStartVertex: int, componentSize: int): def func(): startVertex = graph._verticesWithID[49] destinationVertex = graph._verticesWithID[20] try: vertexPath = [v for v, w in startVertex.ShortestPathToByWeight(destinationVertex)] except DestinationNotReachable: pass # print(f"path length: {len(vertexPath)}") # self.assertEqual(6, len(vertexPath)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) pyTooling-8.11.0/tests/performance/Graph/NetworkX.py000066400000000000000000000144711513317154500224400ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.Graph.""" from pathlib import Path import networkx.exception from networkx import DiGraph as nx_DiGraph, dfs_preorder_nodes, bfs_predecessors, shortest_path from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Graph(PerformanceTest): def test_AddEdge_Flat(self) -> None: def wrapper(count: int): def func(): g = nx_DiGraph() g.add_node(0) for i in range(1, count): g.add_edge(0, i) return func self.runSizedTests(wrapper, self.counts) def test_AddEdge_Linear(self) -> None: def wrapper(count: int): def func(): g = nx_DiGraph() prev = 0 g.add_node(prev) for i in range(1, count): g.add_edge(prev, i) prev = i return func self.runSizedTests(wrapper, self.counts) class RandomGraph(PerformanceTest): def ConstructGraphFromEdgeListFile(self, file: Path, vertexCount: int) -> nx_DiGraph: graph = nx_DiGraph() for v in range(vertexCount): graph.add_node(v) with file.open("r", encoding="utf-8") as f: for line in f.readlines(): v, u, w = line.split(" ") graph.add_edge(int(v), int(u), weight=int(w)) return graph def test_BFS(self) -> None: def wrapper(graph: nx_DiGraph, componentStartVertex: int, componentSize: int): def func(): bfsList = [v for v in bfs_predecessors(graph, componentStartVertex)] self.assertEqual(componentSize - 1, len(bfsList)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_DFS(self) -> None: def wrapper(graph: nx_DiGraph, componentStartVertex: int, componentSize: int): def func(): dfsList = [v for v in dfs_preorder_nodes(graph, componentStartVertex)] self.assertEqual(componentSize, len(dfsList)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_ShortestPathByHops(self) -> None: def wrapper(graph: nx_DiGraph, componentStartVertex: int, componentSize: int): def func(): try: vertexPath = shortest_path(graph, 49, 20) except networkx.exception.NetworkXNoPath: pass # print(f"path length: {len(vertexPath)}") # self.assertEqual(6, len(vertexPath)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_ShortestPathByWeight(self) -> None: def wrapper(graph: nx_DiGraph, componentStartVertex: int, componentSize: int): def func(): try: vertexPath = shortest_path(graph, 49, 20, "weight") except networkx.exception.NetworkXNoPath: pass # print(f"path length: {len(vertexPath)}") # self.assertEqual(6, len(vertexPath)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) pyTooling-8.11.0/tests/performance/Graph/__init__.py000066400000000000000000000142541513317154500224150ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.Graph.""" import timeit from dataclasses import dataclass from pathlib import Path from statistics import median from time import perf_counter_ns from typing import Callable, Iterable from unittest import TestCase from pyTooling.Graph import Graph as pt_Graph if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) @dataclass class BiggestNetwork: startNodeID: int size: int @dataclass class EdgeFile: vertexCount: int edgeCount: int biggestNetwork: BiggestNetwork file: Path class PerformanceTest(TestCase): counts: Iterable[int] = (10, 100, 1000, 10000) edgeFiles: Iterable[EdgeFile] = ( EdgeFile( 100, 150, BiggestNetwork( 92, 72), Path("graph_n100_m150_dir_w0_100.edgelist")), EdgeFile( 1000, 1500, BiggestNetwork( 489, 626), Path("graph_n1000_m1500_dir_w0_100.edgelist")), EdgeFile( 10000, 15000, BiggestNetwork(3056, 5741), Path("graph_n10000_m15000_dir_w0_100.edgelist")), # EdgeFile(100000, 150000, BiggestNetwork(9671, 58243), Path("graph_n100000_m150000_dir_w0_100.edgelist")), ) @staticmethod def minMaxSumMean(array): minimum = 1.0e9 maximum = 0.0 sum = 0.0 for value in array: minimum = value if value < minimum else minimum maximum = value if value > maximum else maximum sum += value return minimum, maximum, sum, sum/len(array) def runSizedTests(self, func: Callable[[int], Callable[[], None]], counts: Iterable[int]): print() print(f" min mean median max") for count in counts: results = timeit.repeat(func(count), repeat=20, number=50) norm = count / 10 minimum, maximum, _, mean = self.minMaxSumMean(results) print(f"{count:>6}x: {minimum/norm:.6f} s {mean/norm:.6f} s {median(results)/norm:.6f} s {maximum/norm:.6f} s") def runFileBasedTests(self, setup: Callable[[Path, int], pt_Graph], func: Callable[[pt_Graph, int, int], Callable[[], None]], edgeFiles: Iterable[EdgeFile]): print() print(f" min mean median max construct") for edgeFile in edgeFiles: file = Path("tests/data/Graph/EdgeLists") / edgeFile.file start = perf_counter_ns() graph = setup(file, edgeFile.vertexCount) construct = (perf_counter_ns() - start) / 1e9 results = timeit.repeat(func(graph, edgeFile.biggestNetwork.startNodeID, edgeFile.biggestNetwork.size), repeat=20, number=50) norm = edgeFile.biggestNetwork.size minimum, maximum, _, mean = self.minMaxSumMean(results) print(f"{edgeFile.vertexCount:>6}x: {minimum/norm:.6f} s {mean/norm:.6f} s {median(results)/norm:.6f} s {maximum/norm:.6f} s {construct/norm:.6f} s") pyTooling-8.11.0/tests/performance/Graph/igraph.py000066400000000000000000000143641513317154500221320ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.Graph.""" from pathlib import Path from igraph import Graph as iGraph from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Graph(PerformanceTest): def test_AddEdge_Flat(self) -> None: def wrapper(count: int): def func(): g = iGraph(directed=True) g.add_vertex("0") for i in range(1, count): i = str(i) g.add_vertex(i) g.add_edge(0, i) return func self.runSizedTests(wrapper, self.counts[:-1]) def test_AddEdge_Linear(self) -> None: def wrapper(count: int): def func(): g = iGraph(directed=True) prev = "0" g.add_vertex(prev) for i in range(1, count): i = str(i) g.add_vertex(i) g.add_edge(prev, i) prev = i return func self.runSizedTests(wrapper, self.counts[:-1]) class RandomGraph(PerformanceTest): def ConstructGraphFromEdgeListFile(self, file: Path, vertexCount: int) -> iGraph: graph = iGraph(directed=True) for v in range(vertexCount): graph.add_vertex(str(v)) with file.open("r", encoding="utf-8") as f: for line in f.readlines(): v, u, w = line.split(" ") graph.add_edge(v, u, weight=int(w)) return graph def test_BFS(self) -> None: def wrapper(graph: iGraph, componentStartVertex: int, componentSize: int): def func(): bfsList = [v for v in graph.bfsiter(componentStartVertex)] self.assertEqual(componentSize, len(bfsList)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_DFS(self) -> None: def wrapper(graph: iGraph, componentStartVertex: int, componentSize: int): def func(): dfsList = [v for v in graph.dfsiter(componentStartVertex)] self.assertEqual(componentSize, len(dfsList)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_ShortestPathByHops(self) -> None: def wrapper(graph: iGraph, componentStartVertex: int, componentSize: int): def func(): try: vertexPath = graph.distances("49", "20") except KeyError: pass # print(f"path length: {len(vertexPath)}") # self.assertEqual(6, len(vertexPath)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) def test_ShortestPathByWeight(self) -> None: def wrapper(graph: iGraph, componentStartVertex: int, componentSize: int): def func(): try: vertexPath = graph.distances("49", "20", "weight") except KeyError: pass # print(f"path length: {len(vertexPath)}") # self.assertEqual(6, len(vertexPath)) return func self.runFileBasedTests(self.ConstructGraphFromEdgeListFile, wrapper, self.edgeFiles) pyTooling-8.11.0/tests/performance/LinkedList/000077500000000000000000000000001513317154500212775ustar00rootroot00000000000000pyTooling-8.11.0/tests/performance/LinkedList/DoublyPyLinkedList.py000066400000000000000000000120531513317154500254040ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_)_ __ | | _____ __| | | (_)___| |_ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | | '_ \| |/ / _ \/ _` | | | / __| __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | | | | < __/ (_| | |___| \__ \ |_ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|_| |_|_|\_\___|\__,_|_____|_|___/\__| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.LinkedList.""" from typing import List from doubly_py_linked_list import DoublyLinkedList from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Insertion(PerformanceTest): def test_InsertBeforeFirst(self) -> None: def wrapper(count: int): def func(): dll = DoublyLinkedList() for i in range(1, count): dll.insert_head(i) return func self.runSizedTests(wrapper, self.counts) def test_InsertAfterLast(self) -> None: def wrapper(count: int): def func(): dll = DoublyLinkedList() for i in range(1, count): dll.insert_tail(i) return func self.runSizedTests(wrapper, self.counts) class Remove(PerformanceTest): def test_FillBuckets(self) -> None: limit = 145 def wrapper(count: int): def func(): dll = DoublyLinkedList(self.randomArray[0:count]) index = 0 collected = 0 buckets = [] buckets.append([]) # dll.Sort(reverse=True) while True: items: List[int] = [] for pos, value in enumerate(dll): if collected + value > limit: continue collected += value buckets[index].append(value) items.append(pos) if collected == limit: break index += 1 if dll.length > len(items): collected = 0 buckets.append([]) nodes = dll.nodes() for pos in items: dll.remove(nodes[pos]) else: break return func self.runSizedTests(wrapper, self.counts[:-1]) pyTooling-8.11.0/tests/performance/LinkedList/LinkedList.py000066400000000000000000000117431513317154500237210ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_)_ __ | | _____ __| | | (_)___| |_ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | | '_ \| |/ / _ \/ _` | | | / __| __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | | | | < __/ (_| | |___| \__ \ |_ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|_| |_|_|\_\___|\__,_|_____|_|___/\__| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.LinkedList.""" from pyTooling.LinkedList import LinkedList as pt_LinkedList, Node as pt_Node from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Insertion(PerformanceTest): def test_InsertBeforeFirst(self) -> None: def wrapper(count: int): def func(): ll = pt_LinkedList() for i in range(1, count): ll.InsertBeforeFirst(pt_Node(i)) return func self.runSizedTests(wrapper, self.counts) def test_InsertAfterLast(self) -> None: def wrapper(count: int): def func(): ll = pt_LinkedList() for i in range(1, count): ll.InsertAfterLast(pt_Node(i)) return func self.runSizedTests(wrapper, self.counts) class Remove(PerformanceTest): def test_FillBuckets(self) -> None: limit = 145 def wrapper(count: int): def func(): ll = pt_LinkedList(pt_Node(i) for i in self.randomArray[0:count]) index = 0 collected = 0 buckets = [] buckets.append([]) ll.Sort(reverse=True) while True: for node in ll.IterateFromFirst(): if collected + node.Value > limit: continue collected += node.Value buckets[index].append(node.Value) node.Remove() if collected == limit: break index += 1 if not ll.IsEmpty: collected = 0 buckets.append([]) else: break return func self.runSizedTests(wrapper, self.counts[:-1]) pyTooling-8.11.0/tests/performance/LinkedList/__init__.py000066400000000000000000001246111513317154500234150ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_)_ __ | | _____ __| | | (_)___| |_ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | | '_ \| |/ / _ \/ _` | | | / __| __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | | | | < __/ (_| | |___| \__ \ |_ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|_| |_|_|\_\___|\__,_|_____|_|___/\__| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.LinkedList.""" import timeit from statistics import median from typing import Callable, Iterable from unittest import TestCase if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class PerformanceTest(TestCase): counts: Iterable[int] = (10, 100, 1000, 10000) randomArray = [ 22, 43, 49, 37, 2, 30, 19, 3, 12, 45, 46, 17, 49, 49, 1, 26, 10, 1, 42, 14, 44, 9, 49, 26, 7, 21, 8, 1, 9, 24, 5, 26, 0, 49, 18, 11, 22, 27, 38, 6, 3, 31, 17, 10, 8, 30, 9, 50, 44, 30, 42, 21, 13, 37, 10, 11, 28, 43, 5, 31, 23, 24, 43, 44, 13, 37, 38, 0, 40, 48, 48, 29, 11, 45, 4, 18, 14, 37, 42, 31, 32, 23, 45, 21, 49, 21, 38, 32, 2, 12, 0, 28, 16, 18, 43, 42, 5, 18, 40, 46, 44, 1, 12, 21, 8, 25, 22, 42, 3, 13, 23, 5, 19, 9, 32, 37, 5, 24, 27, 24, 16, 43, 22, 23, 27, 36, 3, 33, 2, 7, 18, 42, 37, 15, 19, 19, 23, 10, 46, 8, 5, 23, 9, 0, 1, 26, 48, 26, 14, 42, 42, 20, 33, 0, 37, 5, 32, 20, 17, 11, 24, 11, 3, 44, 21, 15, 1, 5, 31, 25, 45, 38, 28, 41, 18, 43, 45, 42, 50, 33, 41, 26, 37, 38, 9, 1, 25, 33, 45, 23, 32, 4, 9, 10, 34, 36, 7, 42, 35, 39, 15, 17, 24, 40, 50, 34, 15, 47, 20, 28, 38, 34, 6, 48, 36, 37, 20, 35, 2, 3, 25, 4, 46, 25, 14, 23, 9, 0, 14, 3, 38, 1, 48, 12, 32, 23, 50, 19, 13, 32, 3, 11, 10, 20, 0, 26, 18, 41, 8, 43, 36, 48, 35, 27, 46, 44, 19, 47, 2, 8, 12, 15, 0, 24, 22, 12, 6, 39, 30, 46, 11, 17, 31, 7, 32, 17, 0, 18, 5, 23, 38, 16, 4, 30, 28, 17, 31, 14, 11, 24, 13, 6, 18, 39, 9, 29, 20, 32, 4, 22, 37, 5, 20, 25, 36, 8, 39, 21, 14, 46, 37, 45, 46, 47, 25, 27, 15, 41, 22, 6, 35, 32, 30, 28, 27, 35, 29, 9, 5, 34, 1, 37, 5, 13, 31, 10, 36, 31, 48, 11, 11, 6, 1, 6, 43, 36, 37, 38, 20, 4, 28, 48, 14, 13, 15, 28, 36, 0, 46, 36, 42, 33, 14, 12, 18, 43, 25, 48, 18, 45, 16, 47, 16, 32, 32, 5, 1, 18, 10, 5, 43, 20, 9, 24, 32, 33, 29, 37, 11, 44, 46, 2, 2, 48, 17, 10, 7, 47, 39, 10, 3, 33, 4, 50, 38, 13, 7, 23, 46, 11, 46, 36, 50, 3, 29, 45, 41, 39, 20, 40, 3, 48, 43, 32, 3, 15, 10, 37, 42, 32, 12, 35, 14, 40, 18, 10, 35, 47, 26, 13, 50, 27, 13, 45, 5, 18, 31, 0, 49, 27, 39, 0, 5, 49, 29, 29, 15, 50, 6, 21, 37, 40, 8, 50, 36, 31, 26, 37, 41, 5, 34, 15, 29, 24, 1, 7, 1, 41, 23, 5, 8, 12, 41, 44, 24, 22, 48, 5, 42, 39, 32, 29, 47, 6, 7, 43, 0, 41, 0, 23, 15, 46, 14, 2, 47, 46, 4, 27, 16, 5, 22, 13, 38, 32, 39, 29, 47, 18, 19, 14, 27, 43, 10, 45, 25, 31, 18, 39, 2, 37, 25, 32, 13, 8, 15, 18, 13, 27, 16, 1, 43, 11, 47, 41, 47, 3, 42, 29, 50, 31, 14, 26, 13, 12, 42, 30, 24, 2, 5, 7, 26, 3, 7, 13, 36, 13, 3, 31, 32, 13, 11, 32, 28, 41, 8, 37, 29, 36, 42, 15, 27, 6, 14, 10, 50, 20, 13, 50, 41, 24, 24, 14, 10, 1, 1, 9, 2, 46, 17, 42, 27, 9, 3, 40, 4, 30, 31, 14, 18, 11, 46, 31, 42, 40, 27, 45, 0, 17, 22, 42, 17, 26, 20, 23, 33, 40, 12, 47, 1, 39, 48, 15, 7, 22, 16, 18, 6, 2, 46, 30, 29, 35, 32, 5, 11, 26, 6, 33, 5, 12, 50, 33, 41, 22, 5, 37, 45, 40, 2, 29, 37, 29, 3, 27, 25, 31, 41, 27, 9, 2, 4, 10, 9, 23, 13, 49, 23, 28, 1, 25, 50, 34, 22, 47, 49, 18, 0, 37, 46, 50, 17, 32, 12, 40, 27, 15, 17, 24, 14, 49, 15, 48, 3, 18, 25, 39, 10, 9, 45, 42, 3, 40, 37, 29, 23, 50, 4, 6, 28, 39, 31, 7, 8, 17, 33, 34, 1, 23, 20, 28, 49, 36, 45, 37, 34, 45, 21, 18, 50, 40, 38, 32, 48, 41, 17, 23, 28, 27, 4, 1, 21, 49, 0, 35, 46, 49, 11, 38, 39, 1, 16, 36, 42, 19, 50, 10, 20, 49, 43, 23, 29, 33, 13, 47, 50, 28, 10, 3, 43, 15, 14, 45, 50, 42, 45, 9, 40, 9, 49, 10, 28, 7, 28, 9, 12, 23, 34, 37, 14, 25, 28, 31, 45, 49, 40, 41, 13, 38, 31, 8, 22, 3, 31, 48, 0, 48, 37, 37, 32, 43, 15, 0, 11, 14, 36, 27, 28, 21, 24, 3, 14, 22, 5, 9, 44, 38, 10, 3, 3, 48, 39, 20, 8, 20, 30, 43, 41, 11, 19, 11, 32, 16, 20, 47, 1, 0, 10, 49, 35, 16, 9, 6, 18, 1, 29, 36, 8, 21, 13, 30, 13, 35, 42, 6, 0, 41, 47, 3, 2, 10, 31, 7, 25, 49, 42, 41, 7, 11, 21, 3, 42, 14, 15, 9, 11, 0, 40, 21, 10, 42, 5, 37, 19, 30, 22, 13, 19, 1, 38, 2, 16, 12, 27, 36, 48, 46, 47, 13, 32, 10, 14, 2, 35, 31, 44, 34, 25, 11, 19, 19, 36, 8, 21, 36, 26, 38, 49, 30, 35, 19, 33, 8, 23, 14, 3, 13, 44, 31, 31, 49, 27, 7, 25, 6, 9, 1, 4, 11, 14, 38, 14, 44, 42, 12, 4, 6, 12, 10, 5, 15, 39, 28, 30, 37, 19, 36, 47, 17, 16, 7, 12, 1, 5, 34, 6, 22, 46, 45, 25, 44, 37, 4, 38, 41, 4, 11, 17, 6, 34, 30, 21, 50, 2, 47, 29, 18, 14, 1, 25, 49, 20, 37, 42, 15, 49, 15, 17, 3, 22, 30, 21, 36, 8, 10, 17, 8, 44, 46, 43, 3, 42, 27, 31, 40, 49, 41, 26, 18, 9, 17, 2, 24, 42, 24, 31, 41, 37, 5, 2, 30, 45, 15, 45, 32, 12, 41, 10, 20, 25, 29, 14, 48, 34, 10, 38, 7, 10, 42, 34, 1, 49, 29, 48, 34, 41, 13, 37, 38, 4, 45, 34, 49, 24, 12, 42, 25, 30, 33, 32, 47, 13, 22, 6, 3, 40, 4, 39, 15, 22, 25, 11, 6, 28, 30, 43, 13, 32, 10, 26, 40, 46, 27, 39, 14, 21, 33, 36, 35, 40, 42, 31, 40, 32, 45, 5, 15, 12, 20, 50, 12, 9, 23, 28, 4, 8, 50, 1, 12, 42, 33, 1, 32, 2, 16, 41, 33, 12, 21, 28, 30, 40, 38, 15, 22, 29, 39, 42, 1, 39, 17, 44, 47, 5, 18, 5, 10, 35, 15, 8, 41, 17, 20, 32, 15, 7, 19, 27, 5, 24, 43, 28, 39, 21, 44, 27, 31, 14, 41, 34, 21, 13, 14, 2, 31, 22, 38, 17, 16, 49, 21, 30, 17, 22, 13, 28, 47, 25, 38, 36, 44, 2, 24, 4, 45, 5, 47, 36, 43, 4, 7, 31, 0, 19, 1, 11, 23, 42, 35, 35, 49, 16, 33, 23, 20, 17, 22, 21, 27, 46, 21, 46, 26, 1, 43, 8, 25, 12, 20, 1, 4, 10, 44, 29, 27, 1, 8, 31, 14, 9, 37, 45, 23, 9, 3, 41, 41, 40, 40, 11, 15, 32, 17, 39, 3, 45, 14, 29, 23, 43, 22, 2, 50, 22, 39, 46, 11, 39, 13, 25, 50, 14, 12, 2, 24, 40, 37, 9, 24, 14, 21, 27, 39, 39, 38, 49, 12, 26, 30, 41, 41, 28, 49, 38, 24, 25, 42, 11, 20, 13, 10, 25, 12, 28, 45, 26, 14, 34, 47, 16, 22, 14, 27, 44, 29, 2, 24, 47, 2, 25, 11, 12, 39, 2, 44, 18, 25, 38, 46, 37, 29, 50, 49, 26, 38, 47, 18, 30, 34, 10, 45, 29, 38, 18, 41, 10, 6, 9, 11, 11, 7, 11, 44, 6, 43, 32, 18, 29, 22, 28, 11, 15, 28, 11, 12, 26, 12, 36, 31, 38, 38, 44, 9, 25, 14, 6, 24, 44, 9, 50, 16, 35, 25, 41, 36, 26, 13, 3, 25, 3, 41, 39, 50, 47, 9, 6, 31, 19, 21, 15, 16, 24, 7, 30, 34, 9, 16, 30, 1, 25, 23, 9, 12, 19, 13, 6, 43, 43, 33, 44, 25, 40, 0, 44, 8, 18, 16, 27, 22, 42, 49, 41, 50, 39, 19, 6, 44, 19, 10, 19, 48, 27, 4, 17, 23, 28, 20, 16, 21, 2, 16, 42, 4, 6, 40, 15, 39, 14, 11, 1, 44, 30, 44, 11, 14, 10, 38, 35, 41, 14, 14, 18, 45, 2, 33, 26, 9, 30, 38, 3, 33, 30, 42, 43, 42, 40, 28, 19, 2, 13, 8, 2, 38, 30, 45, 11, 5, 35, 16, 0, 45, 48, 26, 41, 25, 37, 15, 50, 37, 15, 2, 1, 50, 26, 14, 39, 40, 32, 17, 1, 43, 30, 26, 37, 48, 45, 48, 38, 43, 6, 20, 44, 8, 2, 13, 27, 23, 29, 23, 22, 17, 26, 28, 36, 46, 22, 44, 34, 34, 34, 46, 19, 49, 30, 22, 13, 26, 1, 23, 50, 11, 17, 2, 11, 4, 35, 44, 30, 35, 17, 36, 17, 30, 29, 17, 23, 26, 10, 47, 6, 36, 31, 6, 10, 47, 16, 15, 10, 50, 25, 34, 45, 42, 10, 22, 28, 11, 28, 40, 29, 48, 49, 42, 44, 13, 29, 46, 8, 45, 42, 17, 29, 26, 49, 27, 28, 37, 13, 11, 31, 29, 12, 4, 44, 12, 44, 39, 48, 0, 29, 6, 26, 47, 32, 43, 1, 26, 16, 44, 9, 37, 16, 14, 2, 14, 28, 0, 24, 16, 42, 18, 42, 22, 24, 21, 10, 49, 35, 16, 42, 10, 26, 48, 20, 42, 7, 46, 27, 28, 7, 34, 25, 7, 36, 48, 15, 18, 44, 35, 24, 43, 4, 35, 34, 33, 10, 20, 0, 15, 26, 3, 49, 6, 19, 26, 33, 49, 11, 36, 20, 33, 17, 16, 5, 24, 1, 34, 1, 40, 1, 35, 6, 4, 15, 34, 13, 43, 6, 31, 44, 6, 23, 34, 47, 34, 16, 47, 22, 27, 3, 32, 10, 50, 46, 40, 38, 23, 18, 15, 5, 40, 10, 31, 11, 2, 12, 6, 19, 16, 25, 39, 32, 49, 22, 20, 32, 16, 8, 46, 17, 5, 22, 45, 9, 15, 41, 19, 38, 11, 37, 23, 7, 6, 31, 17, 0, 22, 5, 28, 4, 19, 50, 37, 34, 33, 34, 48, 34, 9, 31, 31, 5, 9, 4, 35, 19, 31, 50, 25, 21, 2, 41, 3, 22, 38, 47, 35, 33, 6, 15, 33, 36, 22, 27, 43, 12, 41, 32, 17, 34, 7, 12, 24, 33, 13, 28, 6, 11, 31, 33, 28, 6, 2, 9, 1, 27, 32, 39, 20, 39, 4, 27, 26, 4, 0, 30, 45, 2, 40, 30, 17, 0, 20, 34, 46, 13, 36, 4, 2, 39, 4, 28, 31, 31, 44, 23, 31, 46, 4, 17, 4, 24, 1, 50, 45, 34, 26, 37, 1, 25, 26, 19, 5, 0, 21, 36, 2, 15, 46, 29, 19, 7, 49, 14, 41, 16, 44, 2, 39, 40, 0, 1, 28, 0, 5, 3, 18, 9, 36, 7, 43, 13, 14, 50, 8, 26, 24, 32, 10, 24, 46, 46, 7, 49, 28, 19, 48, 34, 42, 32, 46, 46, 48, 40, 29, 44, 30, 9, 24, 3, 15, 29, 34, 28, 17, 30, 50, 24, 10, 2, 23, 37, 2, 17, 8, 49, 21, 26, 19, 4, 16, 40, 39, 47, 45, 48, 16, 32, 16, 25, 0, 3, 36, 31, 45, 37, 0, 26, 28, 27, 32, 18, 31, 16, 6, 45, 20, 3, 17, 39, 37, 27, 23, 33, 44, 12, 19, 1, 24, 12, 50, 14, 1, 47, 25, 28, 23, 24, 49, 24, 6, 42, 22, 27, 10, 25, 44, 26, 47, 16, 10, 3, 47, 4, 39, 1, 14, 26, 24, 28, 17, 17, 44, 33, 5, 32, 41, 31, 21, 33, 35, 50, 6, 43, 8, 39, 47, 26, 15, 21, 11, 30, 9, 22, 37, 5, 16, 42, 38, 43, 6, 14, 0, 22, 30, 17, 45, 1, 2, 21, 16, 18, 23, 11, 38, 37, 50, 49, 14, 18, 41, 3, 50, 36, 48, 2, 46, 29, 17, 33, 30, 30, 47, 36, 37, 31, 5, 21, 38, 44, 29, 17, 4, 50, 19, 4, 5, 39, 40, 43, 44, 19, 45, 5, 26, 21, 28, 50, 13, 27, 26, 48, 4, 22, 28, 48, 33, 24, 6, 17, 14, 47, 10, 44, 23, 39, 12, 11, 8, 46, 26, 0, 17, 22, 3, 46, 14, 37, 17, 8, 23, 42, 49, 9, 6, 9, 36, 6, 3, 10, 33, 6, 41, 32, 18, 1, 46, 45, 12, 21, 40, 2, 6, 33, 8, 35, 7, 37, 36, 12, 46, 17, 1, 16, 3, 49, 44, 18, 15, 25, 35, 50, 22, 32, 32, 49, 0, 32, 10, 4, 44, 12, 31, 1, 39, 12, 26, 10, 42, 7, 50, 0, 11, 37, 49, 7, 44, 20, 28, 13, 27, 13, 21, 1, 18, 35, 4, 30, 3, 30, 23, 42, 17, 38, 38, 25, 28, 39, 16, 49, 0, 41, 17, 49, 13, 22, 41, 7, 13, 42, 26, 44, 24, 38, 41, 6, 25, 44, 50, 31, 0, 17, 28, 19, 37, 16, 21, 2, 31, 41, 7, 7, 11, 43, 23, 40, 30, 20, 27, 25, 9, 6, 5, 27, 2, 47, 18, 49, 28, 36, 48, 18, 46, 18, 18, 12, 21, 16, 15, 50, 40, 50, 2, 21, 37, 25, 41, 38, 0, 8, 34, 49, 12, 48, 47, 35, 39, 33, 33, 5, 35, 31, 24, 35, 20, 10, 34, 3, 21, 3, 30, 4, 9, 50, 28, 20, 7, 3, 29, 11, 35, 5, 8, 14, 47, 20, 0, 8, 4, 38, 24, 13, 34, 6, 46, 20, 24, 35, 4, 16, 46, 25, 4, 24, 50, 28, 4, 18, 9, 30, 14, 41, 31, 19, 1, 7, 38, 27, 28, 8, 48, 34, 16, 2, 19, 16, 42, 24, 2, 7, 15, 45, 31, 8, 25, 28, 16, 33, 11, 18, 32, 1, 20, 45, 11, 33, 49, 7, 20, 19, 8, 49, 34, 27, 27, 35, 5, 27, 48, 27, 29, 37, 32, 10, 33, 6, 33, 35, 30, 19, 38, 31, 23, 16, 8, 14, 14, 45, 12, 8, 22, 29, 32, 23, 2, 39, 29, 35, 36, 37, 14, 48, 27, 9, 26, 1, 42, 26, 6, 49, 32, 38, 1, 8, 1, 49, 29, 34, 34, 33, 23, 15, 27, 2, 50, 4, 6, 1, 19, 45, 48, 0, 48, 41, 34, 50, 3, 3, 40, 20, 33, 31, 24, 38, 21, 50, 45, 38, 44, 14, 35, 0, 26, 15, 43, 18, 43, 3, 9, 22, 27, 45, 22, 26, 44, 36, 2, 36, 24, 39, 36, 13, 12, 39, 16, 12, 27, 28, 40, 24, 11, 37, 28, 48, 11, 36, 4, 48, 0, 15, 47, 38, 26, 46, 4, 47, 2, 37, 23, 26, 42, 41, 30, 43, 35, 44, 12, 47, 9, 35, 46, 31, 15, 20, 7, 30, 8, 25, 15, 4, 37, 24, 11, 47, 9, 13, 24, 12, 25, 0, 15, 3, 24, 40, 48, 44, 8, 18, 29, 12, 29, 41, 0, 22, 42, 44, 38, 18, 31, 26, 1, 11, 25, 14, 33, 39, 12, 37, 5, 16, 18, 13, 44, 13, 50, 6, 14, 50, 37, 41, 11, 23, 40, 13, 42, 6, 33, 28, 4, 17, 35, 36, 14, 49, 36, 10, 39, 13, 9, 18, 2, 22, 32, 6, 26, 42, 35, 20, 11, 36, 20, 14, 22, 41, 46, 27, 7, 31, 42, 34, 30, 7, 16, 28, 17, 21, 10, 21, 19, 43, 32, 6, 28, 22, 41, 4, 3, 46, 48, 6, 46, 12, 23, 15, 38, 8, 13, 44, 50, 20, 2, 0, 3, 27, 27, 5, 22, 36, 7, 19, 19, 12, 35, 26, 38, 27, 25, 34, 10, 14, 16, 37, 23, 1, 33, 42, 42, 49, 2, 43, 35, 34, 4, 17, 10, 6, 21, 13, 43, 0, 4, 15, 50, 9, 30, 36, 6, 3, 23, 12, 47, 0, 28, 35, 39, 7, 46, 41, 1, 14, 8, 47, 27, 19, 26, 24, 49, 14, 40, 50, 26, 34, 25, 35, 9, 7, 10, 4, 50, 6, 24, 37, 20, 30, 6, 24, 5, 20, 9, 1, 46, 20, 12, 17, 15, 32, 48, 18, 15, 26, 41, 16, 4, 14, 34, 37, 27, 19, 31, 46, 7, 25, 11, 37, 34, 25, 39, 50, 20, 46, 11, 24, 46, 40, 40, 47, 14, 32, 40, 50, 43, 34, 8, 27, 4, 44, 21, 49, 10, 16, 7, 1, 34, 4, 6, 40, 37, 49, 29, 40, 10, 35, 43, 41, 33, 29, 50, 12, 8, 36, 42, 45, 22, 20, 49, 47, 21, 10, 31, 39, 45, 10, 25, 20, 21, 37, 9, 23, 29, 48, 25, 4, 32, 36, 48, 27, 35, 19, 50, 38, 24, 28, 12, 23, 42, 23, 21, 35, 10, 23, 28, 23, 15, 0, 9, 43, 39, 32, 32, 39, 1, 50, 6, 33, 1, 16, 34, 4, 27, 38, 45, 21, 19, 0, 0, 46, 24, 28, 16, 6, 29, 7, 7, 11, 47, 28, 11, 27, 36, 23, 45, 0, 3, 28, 34, 7, 49, 27, 24, 33, 5, 18, 40, 38, 37, 23, 0, 50, 24, 44, 9, 44, 44, 48, 0, 36, 22, 46, 41, 18, 13, 32, 48, 3, 48, 1, 22, 8, 26, 32, 37, 39, 8, 20, 1, 48, 12, 36, 41, 2, 40, 49, 46, 32, 6, 0, 4, 32, 41, 49, 7, 39, 31, 0, 8, 36, 10, 27, 36, 26, 19, 32, 29, 39, 46, 36, 22, 36, 2, 6, 30, 20, 39, 12, 4, 45, 23, 13, 26, 37, 11, 34, 33, 20, 39, 25, 44, 39, 1, 49, 40, 41, 14, 34, 34, 27, 31, 21, 25, 19, 42, 41, 4, 22, 19, 38, 47, 47, 5, 6, 16, 45, 44, 44, 26, 33, 22, 24, 8, 38, 13, 3, 48, 9, 28, 21, 38, 26, 5, 3, 40, 42, 9, 42, 25, 30, 33, 7, 24, 33, 33, 33, 22, 18, 33, 16, 43, 30, 43, 31, 19, 47, 47, 31, 13, 39, 9, 10, 19, 50, 0, 12, 12, 6, 7, 5, 43, 13, 11, 43, 16, 40, 10, 3, 18, 3, 7, 28, 41, 44, 28, 33, 23, 3, 3, 25, 27, 37, 29, 48, 43, 23, 30, 38, 42, 30, 26, 32, 13, 3, 13, 39, 46, 48, 46, 32, 15, 21, 39, 34, 23, 20, 1, 25, 12, 20, 39, 43, 48, 38, 12, 7, 21, 49, 11, 27, 15, 10, 43, 23, 49, 41, 9, 22, 22, 6, 30, 19, 1, 11, 6, 32, 3, 29, 27, 44, 24, 48, 11, 2, 25, 28, 31, 44, 41, 4, 21, 48, 16, 0, 5, 3, 46, 47, 4, 50, 45, 17, 29, 48, 9, 46, 40, 9, 29, 16, 23, 49, 26, 17, 39, 29, 24, 30, 27, 49, 40, 2, 46, 23, 13, 3, 39, 29, 48, 31, 46, 49, 32, 50, 39, 35, 4, 25, 42, 20, 13, 31, 29, 39, 3, 33, 18, 13, 9, 7, 32, 11, 12, 5, 7, 4, 43, 1, 31, 1, 50, 46, 46, 18, 20, 12, 44, 45, 19, 6, 41, 7, 49, 37, 28, 28, 6, 50, 31, 44, 0, 43, 11, 38, 29, 29, 34, 24, 12, 46, 6, 8, 0, 26, 47, 30, 9, 45, 4, 29, 36, 19, 13, 19, 13, 3, 18, 7, 21, 2, 4, 37, 47, 14, 18, 32, 8, 46, 3, 35, 30, 18, 19, 5, 46, 3, 37, 19, 11, 9, 33, 32, 22, 50, 3, 3, 42, 38, 39, 39, 25, 17, 10, 37, 33, 38, 13, 26, 18, 39, 46, 29, 33, 34, 18, 49, 2, 34, 44, 25, 47, 45, 7, 6, 31, 6, 45, 33, 13, 24, 3, 48, 43, 4, 33, 43, 42, 37, 27, 34, 13, 1, 7, 49, 35, 38, 25, 34, 10, 25, 14, 26, 39, 43, 30, 41, 21, 39, 50, 38, 6, 2, 31, 48, 37, 24, 24, 16, 17, 29, 12, 8, 15, 34, 39, 43, 28, 24, 10, 35, 46, 11, 1, 21, 20, 7, 8, 29, 24, 1, 9, 24, 30, 25, 23, 35, 22, 23, 39, 26, 35, 20, 25, 6, 35, 7, 38, 17, 20, 33, 22, 5, 42, 9, 11, 19, 25, 22, 34, 25, 9, 21, 31, 26, 7, 32, 19, 5, 45, 40, 45, 8, 24, 42, 3, 43, 4, 40, 26, 22, 36, 43, 19, 16, 2, 17, 34, 9, 33, 34, 9, 11, 13, 3, 41, 48, 26, 35, 41, 23, 40, 7, 37, 10, 24, 44, 45, 10, 15, 43, 50, 45, 49, 24, 29, 43, 34, 39, 33, 33, 33, 15, 40, 44, 11, 34, 31, 14, 30, 4, 25, 4, 9, 25, 3, 7, 37, 46, 42, 22, 43, 33, 6, 28, 8, 9, 49, 44, 48, 20, 0, 46, 10, 27, 30, 41, 35, 21, 44, 25, 9, 44, 12, 41, 17, 37, 16, 5, 32, 23, 36, 38, 15, 39, 36, 21, 6, 24, 49, 16, 40, 6, 20, 44, 23, 49, 21, 11, 10, 38, 24, 35, 22, 5, 49, 2, 25, 17, 19, 40, 25, 49, 38, 21, 37, 21, 40, 43, 7, 42, 9, 42, 17, 37, 23, 2, 49, 13, 26, 48, 17, 14, 39, 31, 31, 23, 2, 50, 33, 0, 49, 45, 31, 36, 42, 45, 30, 28, 32, 49, 10, 27, 45, 26, 5, 22, 29, 36, 33, 38, 35, 45, 41, 25, 21, 0, 5, 19, 49, 6, 0, 24, 32, 40, 44, 7, 1, 30, 8, 14, 16, 2, 28, 6, 25, 24, 28, 32, 7, 2, 37, 46, 48, 46, 41, 18, 33, 25, 41, 50, 48, 10, 20, 1, 42, 47, 13, 6, 50, 27, 35, 3, 25, 21, 28, 35, 33, 15, 11, 26, 47, 16, 15, 12, 9, 37, 40, 47, 28, 38, 9, 14, 23, 34, 4, 48, 4, 5, 40, 0, 33, 43, 25, 22, 7, 33, 31, 30, 47, 33, 37, 12, 0, 16, 46, 44, 43, 28, 7, 35, 6, 22, 48, 10, 26, 31, 20, 6, 33, 11, 50, 6, 38, 6, 3, 14, 33, 13, 49, 15, 33, 16, 44, 8, 39, 30, 43, 37, 34, 45, 41, 8, 45, 3, 31, 10, 33, 44, 26, 22, 1, 10, 26, 23, 22, 6, 41, 47, 5, 5, 18, 1, 14, 25, 44, 11, 3, 13, 30, 9, 9, 44, 5, 6, 22, 10, 9, 27, 7, 1, 13, 22, 1, 33, 8, 0, 31, 15, 6, 17, 10, 44, 23, 42, 10, 37, 22, 47, 8, 0, 26, 9, 15, 24, 39, 18, 16, 17, 32, 44, 5, 49, 3, 3, 24, 49, 27, 45, 26, 33, 34, 29, 12, 19, 43, 13, 50, 15, 43, 34, 3, 12, 35, 6, 0, 40, 27, 3, 25, 11, 6, 47, 20, 13, 35, 45, 11, 9, 23, 25, 22, 10, 45, 31, 45, 14, 11, 33, 14, 19, 31, 29, 16, 31, 49, 9, 11, 29, 14, 4, 40, 25, 45, 38, 48, 47, 41, 48, 22, 48, 41, 33, 17, 25, 5, 31, 44, 17, 12, 38, 1, 49, 29, 40, 28, 5, 42, 38, 45, 20, 34, 26, 43, 10, 3, 17, 23, 8, 44, 28, 47, 41, 35, 4, 16, 12, 12, 22, 15, 38, 49, 33, 10, 37, 37, 8, 46, 9, 49, 12, 18, 21, 45, 9, 31, 31, 37, 24, 27, 20, 6, 38, 33, 42, 19, 5, 19, 45, 42, 7, 30, 14, 25, 47, 26, 34, 3, 9, 48, 30, 48, 13, 14, 42, 10, 47, 47, 4, 27, 22, 16, 8, 23, 14, 10, 13, 19, 48, 49, 30, 19, 14, 4, 19, 7, 24, 15, 21, 31, 50, 6, 4, 33, 9, 40, 21, 37, 35, 9, 1, 29, 29, 46, 24, 31, 22, 48, 1, 10, 0, 37, 10, 39, 36, 4, 45, 29, 45, 11, 48, 7, 34, 23, 28, 14, 42, 41, 28, 0, 36, 4, 50, 7, 11, 47, 22, 48, 29, 31, 50, 45, 10, 26, 31, 44, 48, 0, 20, 44, 27, 16, 16, 29, 43, 0, 44, 42, 24, 18, 18, 44, 21, 10, 30, 48, 19, 0, 27, 44, 23, 18, 5, 30, 10, 11, 7, 23, 50, 1, 28, 15, 11, 5, 26, 43, 5, 40, 25, 50, 37, 33, 29, 18, 49, 30, 12, 48, 4, 28, 15, 30, 20, 14, 25, 2, 7, 8, 20, 34, 37, 17, 8, 31, 39, 37, 8, 8, 39, 49, 44, 0, 33, 22, 31, 5, 28, 28, 46, 41, 43, 50, 44, 37, 13, 21, 47, 44, 47, 40, 16, 42, 16, 50, 31, 47, 30, 1, 24, 9, 30, 42, 34, 27, 21, 45, 34, 42, 12, 14, 41, 21, 42, 0, 49, 20, 45, 28, 37, 26, 26, 22, 22, 19, 38, 43, 20, 42, 50, 11, 40, 41, 20, 4, 10, 35, 32, 37, 36, 39, 18, 8, 7, 39, 34, 0, 17, 10, 11, 4, 21, 40, 7, 27, 16, 16, 4, 14, 28, 39, 38, 0, 9, 5, 34, 15, 6, 30, 18, 8, 22, 40, 31, 10, 28, 49, 29, 9, 40, 12, 13, 14, 48, 34, 10, 46, 32, 4, 5, 13, 43, 19, 26, 14, 36, 27, 7, 38, 41, 18, 0, 13, 30, 40, 16, 45, 6, 39, 38, 31, 37, 22, 10, 19, 2, 24, 49, 6, 47, 45, 36, 23, 42, 36, 49, 48, 42, 31, 43, 19, 45, 47, 50, 27, 39, 8, 30, 33, 49, 39, 34, 33, 20, 14, 40, 48, 8, 40, 33, 16, 33, 17, 7, 3, 24, 16, 23, 34, 39, 13, 16, 3, 27, 7, 14, 25, 40, 37, 9, 15, 32, 47, 28, 49, 42, 1, 5, 21, 5, 1, 4, 20, 6, 12, 44, 9, 16, 45, 43, 2, 24, 46, 13, 3, 30, 43, 42, 12, 16, 38, 37, 45, 6, 47, 21, 29, 10, 29, 24, 46, 20, 21, 44, 44, 37, 10, 4, 35, 12, 42, 26, 45, 21, 26, 39, 18, 16, 0, 15, 39, 35, 30, 28, 14, 5, 47, 27, 16, 10, 21, 47, 47, 20, 9, 41, 50, 0, 24, 11, 49, 49, 23, 22, 23, 5, 6, 19, 23, 42, 42, 23, 24, 39, 35, 12, 47, 39, 22, 35, 36, 23, 9, 9, 27, 28, 5, 48, 21, 8, 20, 16, 46, 45, 4, 30, 21, 49, 34, 24, 12, 36, 0, 14, 18, 49, 44, 12, 9, 15, 6, 47, 24, 9, 37, 2, 10, 38, 49, 40, 16, 0, 38, 43, 2, 21, 0, 18, 6, 38, 45, 46, 24, 8, 20, 40, 39, 32, 7, 4, 10, 48, 38, 44, 33, 9, 13, 22, 47, 33, 20, 12, 49, 46, 33, 1, 20, 2, 15, 13, 29, 19, 14, 8, 34, 29, 35, 41, 29, 24, 14, 48, 48, 40, 2, 2, 13, 28, 40, 13, 4, 18, 37, 41, 10, 18, 22, 35, 7, 39, 46, 3, 17, 44, 16, 48, 7, 25, 23, 25, 31, 24, 31, 36, 25, 16, 19, 11, 34, 37, 44, 38, 6, 50, 16, 33, 42, 47, 41, 2, 43, 35, 35, 3, 3, 44, 12, 11, 7, 48, 44, 22, 25, 24, 38, 18, 49, 38, 50, 21, 5, 0, 0, 49, 26, 19, 42, 37, 43, 40, 0, 21, 46, 13, 33, 25, 32, 37, 50, 17, 2, 39, 36, 20, 9, 18, 2, 9, 30, 41, 24, 39, 6, 26, 17, 38, 11, 2, 17, 24, 26, 21, 14, 15, 2, 26, 17, 45, 7, 9, 9, 35, 46, 43, 16, 34, 34, 11, 10, 19, 13, 44, 22, 44, 32, 21, 36, 23, 35, 7, 47, 39, 17, 32, 46, 2, 42, 48, 30, 17, 5, 12, 48, 50, 49, 22, 35, 19, 35, 42, 13, 34, 33, 31, 24, 49, 0, 2, 39, 41, 37, 14, 18, 34, 12, 2, 38, 18, 46, 42, 38, 15, 44, 42, 27, 18, 45, 35, 5, 15, 24, 5, 22, 23, 12, 33, 0, 4, 13, 23, 15, 47, 18, 47, 47, 21, 38, 6, 20, 26, 44, 50, 39, 30, 5, 9, 19, 48, 36, 39, 49, 32, 33, 25, 23, 5, 31, 48, 26, 46, 41, 28, 3, 9, 9, 9, 35, 7, 46, 32, 43, 26, 6, 6, 26, 2, 32, 45, 50, 4, 42, 0, 6, 26, 43, 10, 22, 40, 35, 22, 29, 50, 23, 3, 7, 23, 28, 26, 22, 6, 42, 15, 22, 43, 15, 24, 26, 2, 41, 18, 11, 24, 31, 18, 50, 1, 22, 45, 9, 47, 12, 45, 45, 31, 0, 15, 24, 31, 39, 42, 25, 39, 48, 16, 49, 11, 30, 42, 26, 44, 23, 3, 46, 42, 50, 1, 16, 39, 10, 18, 42, 35, 8, 7, 36, 48, 3, 46, 17, 49, 27, 40, 41, 13, 16, 43, 30, 3, 41, 29, 16, 48, 50, 31, 9, 15, 28, 19, 18, 48, 40, 24, 10, 23, 36, 24, 22, 18, 34, 3, 1, 41, 14, 6, 32, 13, 40, 50, 0, 49, 0, 49, 29, 34, 39, 13, 0, 44, 39, 39, 1, 44, 1, 38, 46, 0, 10, 31, 35, 26, 7, 50, 32, 18, 8, 43, 28, 20, 13, 1, 35, 36, 8, 43, 23, 9, 11, 47, 27, 14, 2, 48, 27, 15, 42, 26, 24, 34, 34, 2, 3, 20, 32, 18, 39, 12, 42, 27, 46, 21, 38, 24, 22, 14, 33, 33, 16, 3, 21, 28, 40, 35, 25, 46, 8, 38, 33, 34, 30, 23, 1, 49, 8, 9, 33, 17, 44, 8, 6, 10, 13, 36, 30, 28, 21, 37, 14, 8, 46, 5, 6, 32, 13, 10, 43, 48, 12, 4, 47, 37, 24, 13, 5, 17, 15, 12, 3, 41, 11, 50, 16, 20, 26, 29, 30, 30, 9, 36, 10, 19, 49, 38, 27, 48, 10, 50, 37, 18, 10, 50, 18, 42, 18, 39, 4, 0, 15, 48, 23, 13, 1, 37, 41, 19, 45, 3, 0, 39, 24, 28, 45, 30, 2, 16, 33, 3, 44, 16, 12, 9, 31, 34, 35, 5, 24, 2, 5, 48, 35, 28, 11, 39, 8, 34, 24, 35, 4, 33, 33, 18, 32, 50, 18, 21, 23, 12, 2, 49, 42, 19, 46, 14, 42, 3, 47, 35, 2, 23, 42, 34, 22, 30, 0, 11, 1, 24, 50, 10, 35, 22, 40, 12, 41, 46, 0, 24, 40, 47, 15, 7, 22, 10, 4, 35, 38, 0, 23, 30, 29, 26, 48, 39, 26, 29, 39, 43, 10, 15, 14, 29, 34, 0, 3, 0, 9, 5, 15, 1, 20, 20, 37, 42, 50, 35, 45, 49, 26, 16, 28, 9, 20, 47, 30, 16, 11, 1, 7, 11, 26, 4, 33, 13, 36, 19, 20, 11, 45, 24, 43, 7, 48, 40, 35, 13, 43, 10, 16, 44, 16, 5, 45, 27, 32, 4, 26, 9, 32, 16, 39, 48, 45, 22, 9, 35, 26, 10, 46, 28, 18, 41, 9, 18, 42, 5, 42, 29, 45, 20, 35, 15, 13, 8, 44, 49, 48, 43, 14, 43, 48, 22, 32, 4, 1, 39, 18, 3, 1, 15, 17, 3, 35, 6, 46, 8, 4, 9, 36, 23, 10, 31, 46, 11, 43, 23, 26, 17, 14, 12, 19, 21, 21, 15, 21, 38, 21, 37, 43, 45, 5, 43, 15, 22, 16, 2, 34, 49, 37, 8, 7, 8, 47, 18, 40, 47, 19, 47, 42, 34, 10, 17, 50, 6, 47, 35, 21, 8, 28, 35, 29, 27, 10, 27, 28, 40, 15, 7, 27, 18, 30, 40, 28, 32, 48, 40, 26, 5, 41, 24, 2, 36, 32, 40, 13, 34, 9, 47, 45, 6, 10, 22, 42, 28, 47, 20, 37, 9, 30, 17, 6, 3, 44, 40, 16, 40, 7, 12, 12, 23, 38, 33, 49, 39, 32, 37, 31, 15, 24, 25, 19, 7, 37, 19, 31, 27, 44, 3, 47, 14, 14, 45, 30, 25, 35, 43, 8, 39, 10, 25, 5, 37, 39, 25, 18, 8, 33, 40, 2, 3, 48, 33, 1, 50, 37, 17, 40, 41, 7, 2, 17, 14, 10, 5, 32, 11, 15, 30, 46, 43, 11, 50, 2, 22, 24, 32, 49, 50, 26, 50, 23, 10, 35, 12, 30, 50, 39, 6, 38, 25, 17, 6, 10, 22, 45, 41, 5, 36, 2, 7, 33, 39, 28, 29, 8, 40, 3, 4, 30, 29, 42, 19, 12, 4, 30, 2, 45, 9, 25, 10, 17, 47, 13, 27, 50, 22, 4, 8, 28, 19, 15, 1, 12, 36, 5, 2, 29, 38, 8, 36, 8, 27, 13, 7, 42, 31, 50, 30, 37, 24, 9, 30, 45, 25, 21, 47, 37, 45, 16, 24, 43, 46, 9, 6, 34, 11, 32, 19, 39, 48, 48, 24, 6, 46, 27, 10, 29, 27, 17, 13, 27, 4, 9, 41, 42, 13, 17, 27, 50, 21, 22, 37, 25, 16, 1, 46, 39, 23, 10, 10, 26, 31, 50, 32, 4, 26, 48, 34, 41, 0, 22, 25, 45, 15, 0, 13, 23, 6, 5, 39, 15, 14, 6, 43, 17, 49, 29, 0, 40, 6, 28, 9, 50, 0, 32, 22, 24, 17, 17, 6, 4, 35, 18, 46, 0, 23, 33, 25, 17, 1, 43, 1, 39, 12, 45, 8, 30, 12, 48, 46, 40, 8, 46, 33, 11, 49, 6, 24, 46, 40, 28, 44, 31, 31, 10, 38, 16, 32, 11, 7, 27, 23, 34, 23, 36, 30, 22, 4, 50, 24, 38, 40, 21, 31, 17, 32, 27, 11, 49, 37, 44, 27, 38, 47, 42, 19, 4, 32, 11, 11, 15, 11, 10, 28, 17, 50, 40, 11, 32, 30, 17, 34, 18, 21, 28, 33, 14, 34, 11, 35, 28, 29, 28, 43, 26, 32, 26, 11, 48, 32, 31, 43, 3, 17, 16, 39, 24, 4, 39, 47, 9, 8, 18, 26, 50, 22, 5, 34, 26, 10, 16, 10, 14, 16, 36, 26, 3, 27, 4, 37, 36, 18, 10, 21, 12, 18, 43, 32, 27, 10, 46, 33, 48, 21, 18, 46, 1, 22, 45, 12, 17, 49, 24, 24, 6, 50, 20, 26, 21, 20, 2, 45, 16, 20, 25, 42, 16, 36, 37, 19, 7, 25, 29, 4, 49, 30, 35, 4, 16, 36, 35, 32, 32, 34, 16, 3, 25, 19, 29, 20, 23, 12, 50, 14, 2, 18, 46, 9, 16, 16, 49, 4, 25, 7, 25, 42, 42, 47, 43, 27, 28, 50, 3, 31, 16, 35, 16, 29, 19, 28, 14, 5, 4, 45, 22, 21, 4, 28, 4, 34, 34, 44, 37, 1, 20, 24, 36, 32, 13, 17, 1, 41, 37, 2, 32, 41, 6, 0, 32, 38, 31, 21, 14, 26, 5, 29, 49, 8, 47, 47, 49, 42, 49, 34, 47, 42, 3, 3, 13, 2, 26, 46, 45, 27, 37, 39, 1, 15, 14, 12, 30, 10, 29, 30, 37, 35, 26, 13, 30, 46, 8, 32, 21, 11, 0, 15, 47, 50, 36, 14, 3, 43, 38, 47, 13, 49, 13, 3, 16, 39, 25, 8, 23, 34, 18, 42, 41, 8, 33, 15, 36, 46, 1, 34, 27, 41, 37, 12, 33, 20, 35, 16, 12, 50, 0, 38, 43, 43, 1, 12, 36, 48, 37, 50, 27, 24, 38, 40, 9, 25, 40, 43, 36, 30, 42, 23, 37, 35, 38, 15, 7, 22, 19, 50, 29, 28, 31, 5, 31, 2, 27, 23, 15, 40, 50, 27, 32, 4, 35, 46, 1, 35, 7, 4, 17, 13, 6, 36, 13, 38, 10, 28, 33, 30, 7, 47, 24, 36, 3, 29, 27, 8, 34, 49, 20, 30, 0, 13, 7, 40, 47, 17, 21, 36, 7, 19, 8, 15, 22, 39, 22, 49, 0, 47, 50, 34, 28, 13, 21, 21, 35, 7, 7, 18, 3, 39, 35, 45, 16, 18, 28, 16, 3, 32, 37, 48, 11, 16, 25, 30, 40, 44, 27, 9, 32, 50, 30, 15, 33, 39, 26, 23, 46, 26, 48, 50, 4, 7, 23, 11, 27, 26, 49, 19, 49, 21, 34, 1, 3, 17, 46, 28, 17, 23, 10, 36, 27, 49, 42, 47, 20, 19, 41, 23, 32, 7, 41, 13, 35, 33, 12, 9, 8, 37, 48, 21, 36, 43, 47, 23, 11, 22, 35, 49, 38, 32, 37, 38, 44, 8, 45, 37, 41, 4, 30, 12, 3, 41, 8, 1, 17, 50, 35, 16, 21, 41, 9, 26, 34, 34, 21, 28, 6, 42, 11, 3, 50, 4, 18, 37, 17, 44, 50, 12, 21, 42, 24, 7, 33, 27, 50, 14, 3, 3, 22, 10, 29, 36, 10, 21, 33, 42, 49, 46, 47, 18, 26, 31, 50, 11, 41, 44, 18, 5, 22, 48, 22, 34, 36, 1, 39, 32, 27, 32, 33, 22, 35, 2, 26, 21, 21, 8, 25, 7, 29, 44, 26, 49, 10, 7, 46, 25, 10, 39, 15, 15, 37, 42, 45, 5, 39, 20, 38, 32, 19, 7, 50, 50, 30, 2, 41, 32, 16, 40, 48, 4, 23, 9, 41, 8, 24, 50, 33, 37, 48, 20, 42, 6, 39, 43, 18, 19, 19, 47, 41, 34, 24, 8, 11, 20, 13, 14, 20, 26, 17, 9, 13, 23, 31, 7, 16, 45, 11, 22, 20, 15, 6, 20, 41, 47, 24, 41, 32, 29, 38, 18, 29, 30, 23, 39, 45, 43, 34, 47, 50, 19, 41, 49, 1, 14, 49, 23, 14, 50, 31, 37, 7, 22, 6, 16, 9, 21, 27, 1, 30, 29, 20, 40, 8, 35, 6, 11, 38, 14, 43, 49, 8, 47, 21, 40, 14, 19, 5, 22, 14, 10, 47, 42, 48, 31, 8, 35, 40, 19, 0, 24, 4, 6, 13, 23, 26, 38, 46, 33, 4, 48, 29, 36, 35, 22, 30, 7, 19, 0, 6, 13, 17, 34, 3, 11, 23, 1, 42, 22, 19, 50, 24, 47, 8, 38, 46, 35, 1, 8, 47, 5, 3, 7, 9, 29, 48, 41, 13, 47, 49, 19, 6, 50, 31, 49, 11, 7, 40, 26, 12, 30, 19, 31, 27, 38, 30, 12, 37, 39, 2, 14, 47, 15, 13, 6, 0, 33, 31, 2, 35, 13, 46, 23, 38, 2, 38, 38, 19, 19, 10, 13, 24, 38, 31, 40, 1, 45, 29, 28, 8, 6, 7, 48, 30, 12, 39, 28, 5, 22, 3, 13, 22, 15, 27, 27, 19, 25, 40, 8, 16, 26, 31, 38, 29, 23, 23, 41, 34, 11, 39, 24, 46, 30, 34, 12, 40, 38, 38, 36, 12, 35, 20, 50, 17, 3, 24, 47, 29, 32, 40, 2, 14, 21, 20, 38, 6, 33, 33, 22, 5, 41, 49, 20, 44, 3, 6, 33, 35, 4, 4, 38, 19, 10, 43, 37, 26, 12, 21, 25, 18, 3, 45, 45, 20, 34, 8, 13, 0, 32, 19, 14, 38, 26, 49, 19, 25, 7, 8, 29, 1, 13, 14, 17, 29, 47, 12, 4, 15, 34, 3, 6, 40, 13, 23, 40, 48, 23, 37, 24, 27, 2, 28, 16, 36, 46, 38, 32, 12, 29, 18, 37, 19, 8, 34, 34, 12, 50, 9, 1, 25, 8, 45, 48, 9, 10, 11, 40, 45, 1, 22, 42, 48, 42, 21, 46, 49, 6, 5, 29, 12, 33, 15, 16, 26, 20, 34, 17, 46, 50, 17, 39, 9, 25, 20, 43, 40, 39, 44, 49, 15, 49, 20, 35, 33, 26, 7, 32, 0, 7, 9, 2, 9, 24, 17, 32, 44, 12, 46, 3, 27, 30, 35, 38, 31, 33, 44, 11, 18, 4, 43, 27, 25, 24, 47, 38, 39, 35, 14, 10, 13, 3, 7, 11, 32, 19, 40, 0, 21, 4, 10, 10, 5, 35, 15, 18, 39, 26, 40, 18, 39, 44, 8, 30, 50, 33, 1, 26, 3, 46, 42, 41, 26, 33, 35, 30, 43, 5, 13, 11, 48, 14, 2, 15, 19, 32, 46, 2, 2, 48, 40, 16, 43, 31, 44, 44, 22, 22, 13, 13, 3, 46, 32, 50, 25, 1, 37, 14, 31, 0, 47, 46, 18, 28, 44, 50, 15, 42, 13, 36, 9, 17, 0, 50, 34, 40, 28, 3, 0, 41, 14, 38, 43, 30, 41, 13, 43, 18, 35, 30, 47, 35, 7, 13, 20, 41, 4, 15, 23, 34, 10, 36, 46, 11, 33, 36, 38, 31, 47, 24, 25, 47, 31, 47, 13, 10, 30, 40, 15, 26, 35, 18, 5, 43, 32, 26, 30, 5, 19, 46, 15, 49, 24, 38, 10, 46, 19, 8, 0, 7, 21, 1, 17, 24, 7, 12, 14, 10, 39, 14, 46, 46, 22, 11, 42, 24, 50, 14, 6, 20, 4, 32, 28, 10, 42, 50, 1, 45, 18, 25, 30, 19, 6, 8, 3, 24, 41, 46, 32, 2, 5, 29, 23, 30, 20, 32, 44, 8, 24, 31, 13, 41, 7, 37, 31, 20, 41, 33, 46, 40, 38, 15, 6, 47, 23, 4, 42, 16, 48, 30, 43, 12, 5, 25, 8, 15, 33, 42, 34, 24, 3, 1, 30, 39, 40, 37, 22, 32, 5, 11, 33, 16, 47, 10, 16, 36, 29, 3, 26, 49, 8, 6, 22, 8, 36, 8, 36, 50, 37, 38, 41, 4, 41, 44, 28, 17, 9, 14, 36, 4, 49, 41, 44, 6, 6, 18, 48, 12, 39, 2, 21, 33, 33, 14, 6, 22, 21, 20, 5, 6, 34, 46, 26, 41, 14, 3, 0, 44, 15, 34, 24, 43, 25, 1, 42, 16, 0, 8, 48, 16, 45, 10, 15, 49, 46, 4, 2, 6, 0, 9, 2, 10, 31, 9, 14, 35, 8, 6, 3, 5, 24, 29, 47, 20, 48, 27, 33, 3, 46, 2, 12, 2, 10, 24, 2, 13, 44, 33, 22, 23, 44, 26, 38, 25, 23, 50, 27, 17, 47, 23, 4, 14, 3, 34, 25, 16, 44, 11, 18, 24, 29, 39, 43, 6, 46, 1, 21, 6, 7, 9, 47, 26, 22, 21, 1, 2, 0, 4, 22, 19, 40, 33, 34, 20, 39, 38, 37, 46, 24, 26, 45, 22, 22, 1, 27, 25, 24, 14, 41, 23, 48, 4, 34, 30, 27, 38, 25, 46, 26, 7, 49, 36, 10, 42, 49, 12, 17, 16, 30, 10, 27, 31, 22, 40, 45, 0, 9, 30, 41, 40, 24, 18, 49, 34, 41, 42, 14, 39, 11, 10, 36, 27, 3, 2, 14, 16, 1, 33, 11, 26, 16, 8, 34, 30, 32, 5, 34, 38, 46, 31, 36, 5, 20, 49, 42, 13, 36, 11, 20, 8, 30, 44, 32, 17, 47, 46, 25, 0, 38, 14, 47, 2, 20, 43, 38, 32, 18, 30, 3, 32, 43, 1, 41, 11, 21, 20, 34, 16, 39, 11, 7, 18, 13, 7, 22, 17, 7, 50, 26, 5, 21, 40, 1, 13, 36, 13, 43, 42, 26, 7, 45, 11, 27, 10, 14, 6, 14, 34, 22, 19, 46, 20, 18, 29, 37, 23, 28, 31, 9, 34, 20, 11, 6, 27, 12, 6, 21, 31, 7, 26, 49, 29, 48, 13, 10, 1, 8, 29, 21, 26, 12, 4, 20, 43, 25, 33, 25, 38, 13, 21, 0, 23, 32, 15, 34, 16, 26, 42, 15, 7, 8, 14, 41, 24, 49, 37, 17, 1, 47, 50, 45, 49, 47, 25, 40, 37, 14, 34, 47, 35, 3, 13, 10, 47, 3, 46, 36, 27, 38, 34, 38, 1, 44, 14, 20, 29, 13, 1, 3, 30, 35, 2, 3, 2, 32, 46, 15, 34, 22, 39, 17, 44, 43, 47, 12, 47, 4, 10, 39, 27, 41, 5, 5, 25, 3, 40, 29, 37, 31, 42, 25, 41, 15, 47, 46, 20, 24, 8, 22, 6, 27, 15, 5, 40, 13, 43, 41, 0, 36, 22, 19, 10, 31, 0, 37, 30, 3, 49, 49, 37, 1, 25, 38, 4, 14, 34, 17, 47, 27, 38, 38, 35, 32, 17, 46, 32, 41, 42, 45, 34, 38, 48, 7, 10, 28, 14, 41, 7, 38, 25, 47, 17, 18, 20, 46, 21, 36, 3, 26, 39, 20, 39, 17, 48, 44, 13, 34, 30, 4, 37, 22, 45, 0, 36, 45, 31, 15, 24, 10, 27, 10, 45, 41, 41, 49, 9, 42, 34, 44, 21, 30, 43, 19, 9, 16, 43, 11, 32, 5, 4, 31, 35, 23, 32, 46, 28, 49, 45, 49, 42, 14, 10, 16, 11, 29, 13, 31, 22, 38, 37, 1, 36, 5, 49, 25, 0, 8, 7, 39, 1, 43, 44, 44, 20, 25, 24, 16, 36, 23, 50, 38, 26, 3, 23, 31, 39, 8, 36, 19, 21, 0, 33, 33, 20, 12, 12, 13, 48, 22, 31, 29, 9, 7, 30, 48, 5, 40, 3, 30, 17, 27, 39, 5, 35, 13, 16, 40, 0, 12, 40, 49, 43, 2, 7, 9, 15, 50, 45, 30, 17, 26, 27, 38, 5, 45, 26, 37, 25, 47, 23, 17, 38, 17, 39, 36, 24, 29, 38, 45, 46, 22, 0, 18, 11, 26, 26, 10, 45, 41, 6, 2, 7, 10, 11, 44, 44, 42, 7, 24, 9, 40, 27, 42, 32, 18, 18, 4, 26, 0, 22, 20, 49, 8, 24, 14, 46, 44, 31, 50, 29, 47, 27, 43, 0, 25, 13, 46, 27, 33, 37, 49, 41, 46, 44, 34, 19, 12, 45, 36, 43, 17, 28, 12, 9, 10, 45, 4, 39, 3, 10, 38, 3, 17, 44, 30, 11, 20, 47, 16, 49, 15, 20, 22, 39, 19, 8, 12, 40, 19, 24, 48, 20, 50, 44, 4, 17, 14, 17, 26, 37, 9, 4, 9, 13, 46, 15, 17, 22, 15, 18, 45, 39, 20, 10, 24, 31, 4, 0, 17, 8, 5, 42, 40, 44, 30, 12, 30, 46, 42, 9, 31, 36, 22, 41, 39, 31, 25, 28, 8, 44, 36, 45, 31, 19, 34, 39, 32, 19, 28, 40, 24, 40, 24, 37, 33, 43, 37, 12, 1, 26, 19, 26, 2, 43, 43, 32, 9, 7, 30, 12, 5, 43, 5, 46, 36, 5, 16, 32, 28, 31, 48, 50, 8, 13, 22, 12, 33, 29, 10, 46, 0, 2, 50, 3, 36, 5, 29, 30, 33, 22, 24, 18, 30, 39, 13, 14, 24, 6, 20, 21, 4, 26, 3, 50, 50, 16, 29, 18, 15, 9, 21, 11, 48, 41, 26, 2, 19, 22, 47, 36, 37, 4, 48, 39, 1, 20, 26, 29, 1, 9, 25, 24, 50, 24, 7, 26, 23, 5, 18, 16, 5, 20, 32, 12, 24, 16, 1, 30, 22, 17, 21, 18, 30, 27, 42, 4, 42, 46, 35, 11, 34, 23, 6, 7, 50, 18, 31, 30, 30, 21, 38, 35, 19, 46, 34, 5, 13, 11, 42, 23, 24, 46, 30, 6, 21, 33, 18, 26, 23, 2, 20, 45, 9, 44, 2, 21, 40, 27, 21, 34, 17, 29, 50, 46, 25, 1, 15, 0, 34, 6, 6, 47, 14, 15, 50, 17, 24, 13, 6, 42, 3, 15, 8, 37, 8, 7, 21, 34, 15, 8, 24, 22, 15, 6, 9, 9, 22, 13, 0, 37, 2, 34, 8, 10, 39, 33, 30, 32, 6, 1, 32, 23, 1, 38, 20, 40, 25, 30, 17, 42, 9, 44, 39, 26, 17, 48, 3, 26, 31, 5, 41, 13, 0, 23, 36, 31, 16, 41, 22, 40, 6, 30, 35, 34, 24, 26, 21, 47, 18, 16, 43, 0, 8, 10, 9, 27, 16, 1, 27, 29, 19, 24, 46, 1, 32, 9, 40, 15, 25, 50, 5, 18, 11, 30, 21, 40, 23, 22, 10, 5, 42, 17, 29, 38, 27, 16, 11, 14, 16, 16, 21, 38, 26, 45, 23, 0, 18, 9, 24, 6, 29, 21, 17, 37, 27, 46, 17, 0, 1, 35, 5, 1, 1, 29, 15, 44, 13, 1, 39, 25, 35, 21, 50, 39, 27, 35, 32, 45, 41, 38, 47, 43, 2, 35, 13, 18, 42, 36, 42, 33, 7, 50, 15, 25, 9, 17, 37, 5, 1, 19, 26, 12, 1, 19, 19, 36, 8, 32, 50, 7, 13, 49, 43, 21, 21, 44, 20, 6, 19, 50, 33, 6, 13, 46, 24, 29, 3, 8, 6, 34, 15, 10, 19, 37, 0, 38, 31, 32, 13, 41, 13, 46, 17, 19, 7, 14, 2, 21, 35, 16, 6, 41, 27, 43, 48, 44, 0, 49, 13, 49, 40, 36, 13, 44, 0, 9, 7, 30, 50, 12, 27, 22, 1, 48, 39, 37, 21, 35, 14, 12, 41, 11, 18, 13, 17, 40, 38, 23, 44, 46, 28, 18, 34, 27, 22, 39, 31, 24, 2, 17, 12, 15, 7, 14, 11, 44, 40, 13, 9, 2, 2, 20, 10, 43, 24, 29, 39, 20, 30, 15, 42, 26, 30, 12, 33, 25, 32, 8, 24, 40, 43, 1, 3, 32, 43, 34, 26, 25, 6, 27, 7, 3, 29, 12, 49, 4, 27, 20, 12, 27, 3, 24, 43, 31, 31, 34, 20, 21, 2, 18, 28, 46, 19, 22, 40, 38, 31, 26, 16, 30, 14, 35, 26, 10, 34, 25, 40, 24, 32, 16, 4, 26, 31, 30, 5, 25, 19, 48, 32, 13, 9, 21, 49, 34, 33, 41, 45, 29, 38, 46, 3, 0, 7, 43, 46, 36, 50, 27, 38, 46, 13, 49, 32, 7, 46, 21, 22, 9, 12, 18, 25, 9, 17, 6, 47, 24, 50, 14, 1, 44, 50, 3, 13, 25, 39, 23, 49, 34, 7, 41, 15, 2, 17, 22, 45, 36, 29, 26, 32, 34, 6, 11, 34, 45, 49, 28, 35, 0, 21, 33, 20, 0, 26, 13, 39, 14, 7, 26, 25, 44, 22, 23, 32, 15, 14, 20, 21, 2, 40, 8, 11, 38, 6, 46, 27, 10, 35, 19, 42, 47, 13, 41, 41, 44, 37, 3, 42, 7, 19, 33, 1, 16, 47, 22, 43, 3, 8, 15, 15, 27, 50, 42, 39, 35, 17, 30, 26, 30, 20, 45, 35, 35, 31, 28, 14, 12, 13, 47, 49, 25, 46, 20, 12, 1, 6, 29, 38, 26, 18, 36, 29, 23, 8, 48, 46, 39, 48, 38, 10, 28, 40, 40, 18, 41, 27, 10, 22, 48, 4, 17, 12, 40, 8, 47, 44, 45, 15, 48, 5, 46, 20, 47, 48, 22, 34, 35, 45, 38, 40, 12, 49, 19, 10, 49, 24, 12, 26, 0, 39, 26, 18, 19, 35, 40, 21, 9, 11, 50, 28, 43, 22, 32, 35, 49, 37, 42, 18, 45, 50, 9, 48, 30, 35, 16, 3, 48, 8, 46, 15, 1, 1, 38, 29, 22, 6, 34, 3, 43, 10, 41, 34, 46, 17, 33, 41, 35, 1, 48, 49, 12, 10, 4, 23, 11, 28, 47, 17, 50, 31, 27, 50, 18, 8, 33, 10, 21, 25, 45, 25, 2, 44, 47, 9, 50, 47, 46, 12, 19, 38, 12, 18, 18, 30, 7, 13, 34, 50, 20, 12, 1, 17, 50, 7, 47, 31, 2, 8, 35, 39, 45, 39, 47, 31, 33, 15, 13, 40, 46, 22, 27, 16, 23, 44, 29, 32, 16, 34, 6, 13, 45, 22, 46, 23, 24, 30, 29, 28, 7, 45, 8, 20, 4, 33, 50, 49, 0, 43, 37, 27, 14, 46, 9, 10, 39, 46, 32, 43, 3, 10, 21, 1, 23, 35, 1, 2, 6, 14, 50, 26, 16, 6, 46, 23, 35, 28, 43, 5, 6, 30, 21, 7, 9, 35, 1, 14, 11, 41, 10, 22, 9, 7, 46, 44, 35, 10, 43, 26, 14, 42, 1, 32, 45, 47, 37, 8, 6, 25, 21, 22, 35, 26, 37, 46, 40, 9, 12, 7, 46, 50, 5, 26, 3, 34, 15, 5, 3, 49, 25, 9, 31, 48, 26, 50, 44, 23, 12, 44, 38, 19, 44, 23, 44, 19, 5, 19, 34, 7, 0, 23, 27, 27, 38, 3, 7, 8, 22, 18, 10, 12, 3, 1, 2, 36, 12, 25, 4, 2, 5, 16, 48, 7, 24, 36, 50, 47, 25, 21, 15, 12, 30, 10, 29, 33, 44, 36, 18, 28, 39, 36, 35, 11, 39, 0, 23, 4, 32, 16, 13, 31, 32, 11, 7, 7, 23, 45, 9, 5, 50, 41, 9, 35, 13, 39, 7, 1, 40, 47, 28, 6, 7, 33, 39, 34, 21, 3, 34, 21, 7, 47, 22, 30, 26, 15, 30, 50, 14, 22, 34, 9, 11, 28, 36, 7, 14, 27, 43, 4, 39, 37, 29, 13, 44, 14, 1, 23, 38, 25, 50, 19, 46, 1, 8, 9, 6, 27, 46, 16, 8, 4, 38, 9, 22, 15, 46, 29, 1, 46, 2, 30, 49, 37, 40, 19, 20, 21, 37, 4, 9, 30, 33, 42, 31, 18, 3, 13, 39, 37, 24, 40, 17, 44, 21, 17, 28, 41, 14, 11, 1, 19, 18, 15, 21, 45, 29, 25, 24, 3, 25, 27, 5, 21, 33, 40, 6, 0, 15, 48, 26, 27, 4, 49, 47, 9, 43, 14, 2, 6, 37, 48, 24, 49, 4, 4, 41, 44, 27, 30, 6, 14, 30, 17, 17, 42, 15, 32, 5, 10, 33, 33, 28, 18, 2, 1, 19, 6, 31, 48, 43, 42, 37, 29, 33, 43, 16, 46, 6, 35, 2, 46, 36, 35, 27, 34, 36, 1, 2, 14, 6, 39, 7, 19, 18, 41, 47, 10, 43, 33, 7, 26, 46, 40, 46, 11, 27, 27, 35, 34, 40, 49, 19, 40, 26, 6, 34, 42, 6, 10, 38, 48, 30, 40, 30, 12, 17, 7, 11, 32, 47, 34, 14, 36, 16, 3, 39, 18, 26, 23, 31, 5, 31, 25, 14, 32, 37, 40, 46, 35, 26, 29, 25, 33, 41, 8, 11, 0, 2, 7, 16, 0, 50, 44, 38, 47, 20, 49, 0, 29, 9, 2, 23, 10, 12, 35, 31, 24, 7, 5, 43, 0, 39, 37, 6, 21, 1, 21, 17, 3, 50, 34, 49, 8, 21, 19, 26, 28, 41, 47, 3, 36, 32, 23, 15, 45, 42, 26, 39, 2, 6, 6, 31, 41, 15, 40, 36, 23, 19, 35, 11, 40, 25, 4, 12, 4, 44, 3, 43, 50, 21, 24, 9, 18, 30, 29, 10, 45, 33, 45, 16, 32, 36, 45, 33, 42, 43, 35, 33, 32, 44, 47, 46, 0, 14, 29, 8, 40, 30, 47, 38, 41, 26, 43, 43, 2, 12, 10, 42, 24, 9, 27, 14, 41, 22, 26, 50, 48, 12, 21, 29, 19, 16, 0, 49, 40, 34, 24, 41, 26, 31, 48, 49, 20, 35, 24, 20, 14, 19, 18, 18, 19, 10, 41, 42, 32, 45, 47, 30, 38, 36, 46, 17, 35, 11, 16, 30, 3, 19, 30, 47, 1, 13, 1, 26, 22, 48, 44, 23, 34, 27, 33, 3, 27, 1, 34, 47, 1, 5, 48, 42, 9, 8, 8, 10, 0, 2, 20, 7, 20, 25, 48, 27, 12, 19, 32, 46, 44, 29, 32, 10, 49, 26, 43, 48, 6, 45, 33, 29, 4, 41, 13, 42, 34, 28, 7, 27, 31, 12, 0, 17, 37, 45, 33, 17, 43, 2, 33, 30, 48, 2, 2, 31, 2, 3, 50, 17, 28, 8, 47, 41, 49, 14, 38, 30, 2, 10, 21, 34, 22, 45, 14, 44, 27, 50, 8, 46, 21, 10, 6, 19, 37, 13, 38, 29, 37, 37, 47, 16, 14, 21, 25, 7, 9, 35, 35, 23, 22, 40, 34, 21, 49, 48, 43, 15, 36, 43, 44, 18, 48, 21, 34, 40, 22, 16, 28, 7, 40, 50, 17, 33, 24, 7, 33, 30, 18, 11, 12, 0, 5, 17, 16, 35, 37, 0, 16, 45, 41, 21, 16, 0, 38, 12, 40, 41, 33, 13, 31, 6, 41, 49, 27, 2, 12, 39, 3, 34, 13, 48, 35, 14, 24, 13, 48, 9, 30, 5, 40, 7, 19, 0, 34, 35, 15, 43, 27, 22, 14, 5, 21, 19, 24, 3, 16, 37, 0, 15, 0, 34, 26, 9, 17, 45, 32, 37, 46, 35, 29, 6, 7, 37, 17, 45, 43, 2, 39, 0, 46, 40, 20, 34, 27, 8, 47, 0, 2, 3, 33, 5, 5, 5, 49, 32, 47, 21, 18, 48, 30, 0, 36, 7, 6, 3, 35, 4, 10, 13, 39, 20, 41, 26, 25, 35, 22, 43, 49, 48, 4, 32, 10, 18, 46, 2, 12, 49, 40, 37, 36, 9, 39, 7, 46, 9, 4, 25, 27, 5, 35, 45, 4, 7, 33, 8, 38, 6, 16, 12, 24, 31, 30, 49, 9, 42, 33, 38, 31, 26, 45, 28, 12, 21, 48, 5, 3, 22, 16, 47, 20, 16, 43, 17, 34, 50, 28, 46, 17, 1, 14, 25, 19, 46, 32, 0, 42, 6, 47, 33, 4, 5, 47, 40, 13, 4, 32, 10, 46, 26, 47, 3, 39, 17, 25, 7, 12, 4, 25, 2, 45, 5, 16, 10, 47, 23, 42, 47, 28, 32, 2, 46, 27, 3, 11, 16, 31, 17, 50, 23, 50, 30, 11, 12, 3, 39, 40, 27, 43, 24, 14, 4, 18, 19, 8, 43, 1, 28, 19, 21, 40, 10, 13, 23, 47, 0, 36, 19, 21, 29, 5, 38, 12, 21, 20, 1, 11, 24, 25, 15, 0, 36, 8, 42, 10, 14, 36, 9, 14, 5, 21, 34, 11, 33, 6, 5, 4, 26, 25, 13, 44, 4, 13, 0, 13, 33, 5, 15, 18, 46, 3, 15, 15, 48, 29, 6, 14, 25, 38, 19, 22, 37, 28, 17, 47, 18, 49, 15, 26, 8, 24, 20, 3, 2, 2, 5, 35, 26, 6, 3, 14, 28, 26, 6, 42, 28, 4, 10, 16, 7, 14, 9, 35, 35, 2, 2, 0, 48, 31, 16, 11, 37, 1, 28, 23, 0, 48, 15, 37, 43, 31, 8, 5, 19, 27, 36, 28, 44, 5, 30, 2, 42, 23, 33, 28, 9, 25, 46, 45, 44, 4, 0, 21, 0, 33, 0, 16, 30, 6, 14, 19, 8, 23, 28, 8, 8, 31, 5, 48, 39, 32, 11, 45, 3, 41, 10, 39, 29, 49, 22, 19, 11, 36, 36, 36, 5, 15, 4, 28, 42, 24, 42, 28, 25, 37, 18, 31, 0, 33, 16, 15, 24, 32, 50, 18, 18, 43, 32, 30, 26, 28, 6, 41, 30, 42, 22, 47, 24, 13, 49, 12, 49, 41, 6, 36, 36, 0, 50, 46, 40, 38, 39, 49, 43, 7, 4, 35, 14, 32, 6, 14, 2, 50, 48, 3, 40, 37, 14, 50, 4, 35, 17, 10, 3, 25, 9, 50, 43, 25, 24, 22, 40, 12, 12, 5, 15, 34, 19, 32, 31, 27, 43, 46, 22, 48, 45, 27, 32, 3, 26, 44, 37, 3, 10, 8, 25, 19, 30, 19, 1, 17, 37, 33, 0, 32, 11, 19, 47, 44, 9, 23, 47, 50, 18, 11, 40, 27, 35, 32, 29, 6, 47, 39, 25, 21, 28, 46, 19, 17, 21, 48, 9, 39, 39, 29, 27, 12, 34, 29, 33, 48, 35, 32, 23, 45, 15, 45, 1, 37, 43, 13, 42, 50, 26, 19, 4, 10, 27, 38, 25, 12, 43, 17, 12, 18, 4, 14, 48, 1, 24, 2, 47, 16, 38, 21, 31, 27, 24, 50, 20, 43, 29, 40, 38, 50, 20, 49, 4, 8, 41, 20, 44, 39, 47, 20, 8, 43, 7, 0, 37, 16, 18, 24, 49, 20, 48, 3, 36, 42, 49, 21, 7, 25, 6, 33, 49, 16, 26, 15, 26, 7, 39, 16, 46, 8, 48, 33, 8, 15, 41, 47, 9, 30, 35, 28, 25, 12, 11, 28, 10, 3, 28, 40, 2, 25, 34, 38, 46, 19, 21, 34, 23, 50, 44, 27, 38, 41, 37, 19, 29, 1 ] @staticmethod def minMaxSumMean(array): minimum = 1.0e9 maximum = 0.0 sum = 0.0 for value in array: minimum = value if value < minimum else minimum maximum = value if value > maximum else maximum sum += value return minimum, maximum, sum, sum/len(array) def runSizedTests(self, func: Callable[[int], Callable[[], None]], counts: Iterable[int]): print() print(f" min mean median max") for count in counts: results = timeit.repeat(func(count), repeat=20, number=50) norm = count / 10 minimum, maximum, _, mean = self.minMaxSumMean(results) print(f"{count:>6}x: {minimum/norm:.6f} s {mean/norm:.6f} s {median(results)/norm:.6f} s {maximum/norm:.6f} s") pyTooling-8.11.0/tests/performance/LinkedList/dqueue.py000066400000000000000000000102651513317154500231450ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_)_ __ | | _____ __| | | (_)___| |_ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | | '_ \| |/ / _ \/ _` | | | / __| __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | | | | < __/ (_| | |___| \__ \ |_ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|_| |_|_|\_\___|\__,_|_____|_|___/\__| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.LinkedList.""" from collections import deque from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Insertion(PerformanceTest): def test_InsertBeforeFirst(self) -> None: def wrapper(count: int): def func(): dq = deque() for i in range(1, count): dq.appendleft(i) return func self.runSizedTests(wrapper, self.counts) def test_InsertAfterLast(self) -> None: def wrapper(count: int): def func(): dq = deque() for i in range(1, count): dq.append(i) return func self.runSizedTests(wrapper, self.counts) pyTooling-8.11.0/tests/performance/LinkedList/list.py000066400000000000000000000142671513317154500226360ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_)_ __ | | _____ __| | | (_)___| |_ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | | '_ \| |/ / _ \/ _` | | | / __| __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | | | | < __/ (_| | |___| \__ \ |_ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|_| |_|_|\_\___|\__,_|_____|_|___/\__| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for list.""" from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Insertion(PerformanceTest): def test_InsertBeforeFirst(self) -> None: def wrapper(count: int): def func(): lst = [] for i in range(1, count): lst.insert(0, i) return func self.runSizedTests(wrapper, self.counts) def test_InsertAfterLast(self) -> None: def wrapper(count: int): def func(): lst = [] for i in range(1, count): lst.append(i) return func self.runSizedTests(wrapper, self.counts) class Remove(PerformanceTest): def test_FillBuckets_RemoveList(self) -> None: limit = 145 def wrapper(count: int): def func(): lst =[i for i in self.randomArray[0:count]] index = 0 collected = 0 buckets = [] buckets.append([]) lst.sort(reverse=True) while True: removeList = [] for pos, value in enumerate(lst): if collected + value > limit: continue collected += value buckets[index].append(value) removeList.append(pos) # if collected == limit: # break index += 1 if len(lst) > len(removeList): collected = 0 buckets.append([]) for offset, pos in enumerate(removeList): lst.pop(pos - offset) else: break return func self.runSizedTests(wrapper, self.counts[:-1]) def test_FillBuckets_MoveValue(self) -> None: limit = 145 def wrapper(count: int): def func(): lst = [i for i in self.randomArray[0:count]] index = 0 collected = 0 buckets = [] buckets.append([]) lst.sort(reverse=True) while True: pos = 0 for value in lst: if collected + value > limit: lst[pos] = value pos += 1 continue collected += value buckets[index].append(value) lst = lst[:pos] index += 1 if len(lst) > 0: collected = 0 buckets.append([]) else: break return func self.runSizedTests(wrapper, self.counts[:-1]) def test_FillBuckets_NewList(self) -> None: limit = 145 def wrapper(count: int): def func(): lst =[i for i in self.randomArray[0:count]] index = 0 collected = 0 buckets = [] buckets.append([]) lst.sort(reverse=True) while True: newLst = [] for value in lst: if collected + value > limit: newLst.append(value) continue collected += value buckets[index].append(value) lst = newLst index += 1 if len(lst) > 0: collected = 0 buckets.append([]) else: break return func self.runSizedTests(wrapper, self.counts[:-1]) pyTooling-8.11.0/tests/performance/Tree/000077500000000000000000000000001513317154500201345ustar00rootroot00000000000000pyTooling-8.11.0/tests/performance/Tree/AnyTree.py000066400000000000000000000105071513317154500220600ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _ \/ _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | __/ __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \___|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for anytree.""" from anytree import Node from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Tree(PerformanceTest): def test_SetParent(self) -> None: def wrapper(count: int): def func(): rootNode = Node(0) for i in range(1, count): Node(i, parent=rootNode) return func self.runTests(wrapper, self.counts[:-1]) def test_AddFlatTree(self) -> None: def run(count: int): def func(): trees = [] for i in range(1, 10): parentNode = Node(count * i) for j in range(1, count): _ = Node(count * i + j, parent=parentNode) trees.append(parentNode) rootNode = Node(0) rootNode.children = trees return func self.runTests(run, self.counts[:-1]) pyTooling-8.11.0/tests/performance/Tree/IterTree.py000066400000000000000000000077521513317154500222440ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _ \/ _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | __/ __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \___|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for iterTree.""" from itertree import iTree from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Tree(PerformanceTest): def test_AddChildren(self) -> None: def wrapper(count: int): def func(): rootNode = iTree("root", value=0) for i in range(1, count): rootNode+=iTree("child", value=i) return func self.runTests(wrapper, self.counts) pyTooling-8.11.0/tests/performance/Tree/Node.py000066400000000000000000000131461513317154500214000ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _ \/ _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | __/ __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \___|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.Tree.""" from pyTooling.Tree import Node from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Tree(PerformanceTest): def test_AddChildren(self) -> None: def wrapper(count: int): def func(): rootNode = Node(0) for i in range(1, count): rootNode.AddChild(Node(i)) return func self.runTests(wrapper, self.counts) def test_SetParent(self) -> None: def wrapper(count: int): def func(): rootNode = Node(0) for i in range(1, count): Node(i, parent=rootNode) return func self.runTests(wrapper, self.counts) def test_AddLongAncestorChain(self) -> None: def wrapper(count: int): def func(): parentNode = Node(0) for i in range(1, count): parentNode = Node(i, parent=parentNode) return func self.runTests(wrapper, self.counts) def test_AddLongChildBranch(self) -> None: def wrapper(count: int): def func(): parentNode = Node(0) for i in range(1, count): node = Node(i) parentNode.AddChild(node) parentNode = node return func self.runTests(wrapper, self.counts) def test_Path(self) -> None: def wrapper(count: int): def func(): parentNode = Node(0) for i in range(1, count): parentNode = Node(i, parent=parentNode) leaf = parentNode _ = leaf.Path return func self.runTests(wrapper, self.counts) def test_GetPath(self) -> None: def wrapper(count: int): def func(): parentNode = Node(0) for i in range(1, count): parentNode = Node(i, parent=parentNode) leaf = parentNode _ = [node for node in leaf.GetPath()] return func self.runTests(wrapper, self.counts) def test_AddFlatTree(self) -> None: def run(count: int): def func(): trees = [] for i in range(1, 10): parentNode = Node(count * i) for j in range(1, count): _ = Node(count * i + j, parent=parentNode) trees.append(parentNode) rootNode = Node(0) rootNode.AddChildren(trees) return func self.runTests(run, self.counts[:-1]) pyTooling-8.11.0/tests/performance/Tree/TreeLib.py000066400000000000000000000114311513317154500220340ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _ \/ _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | __/ __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \___|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for treelib.""" from treelib import Tree as treelib_Tree from . import PerformanceTest if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Tree(PerformanceTest): def test_AddChildren(self) -> None: def wrapper(count: int): def func(): tree = treelib_Tree() rootNode = tree.create_node("root", identifier=0, data=0) for i in range(1, count): tree.create_node("child", parent=rootNode, data=i) return func self.runTests(wrapper, self.counts) def test_SetParent(self) -> None: def wrapper(count: int): def func(): tree = treelib_Tree() rootNode = tree.create_node(identifier=0) for i in range(1, count): tree.create_node(identifier=i, parent=0) return func self.runTests(wrapper, self.counts) def test_AddFlatTree(self) -> None: def run(count: int): def func(): tree = treelib_Tree() rootNode = tree.create_node(identifier=0) for i in range(1, 10): newTree = treelib_Tree() parentNode = newTree.create_node(identifier=count * i) for j in range(1, count): newTree.create_node(identifier=count * i + j, parent=parentNode) tree.paste(0, newTree) return func self.runTests(run, self.counts[:-1]) pyTooling-8.11.0/tests/performance/Tree/__init__.py000066400000000000000000000111131513317154500222420ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _ \/ _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | __/ __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \___|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests for pyTooling.Tree.""" import timeit from statistics import median, mean from typing import Callable, Iterable from unittest import TestCase if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class PerformanceTest(TestCase): counts: Iterable[int] = (10, 100, 1000, 10000) @staticmethod def minMaxSumMean(array): minimum = 1.0e9 maximum = 0.0 sum = 0.0 for value in array: minimum = value if value < minimum else minimum maximum = value if value > maximum else maximum sum += value return minimum, maximum, sum, sum / len(array) def runTests(self, func: Callable[[int], Callable[[], None]], counts: Iterable[int]): print() print(f" min mean median max") for count in counts: results = timeit.repeat(func(count), repeat=20, number=50) norm = count / 10 minimum, maximum, _, mean = self.minMaxSumMean(results) print(f"{count:>5}x: {minimum/norm:.6f} s {mean/norm:.6f} s {median(results)/norm:.6f} s {maximum/norm:.6f} s") pyTooling-8.11.0/tests/performance/__init__.py000066400000000000000000000067211513317154500213540ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, | # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Performance tests.""" pyTooling-8.11.0/tests/performance/requirements.txt000066400000000000000000000005671513317154500225310ustar00rootroot00000000000000-r ../../requirements.txt # Coverage collection Coverage ~= 7.13 # Test Runner pytest ~= 9.0 pytest-cov ~= 7.0 # For performance testing of Node (tree) anytree ~= 2.13 itertree ~= 1.1 treelib ~= 1.7 # For performance testing of Graph/Vertex (graph) networkx ~= 3.4 igraph ~= 0.11.0 # For performance testing of LinkedList/Node (linked list) doubly-py-linked-list ~= 1.1 pyTooling-8.11.0/tests/pyPackage.Tool/000077500000000000000000000000001513317154500175545ustar00rootroot00000000000000pyTooling-8.11.0/tests/pyPackage.Tool/__init__.py000066400000000000000000000004601513317154500216650ustar00rootroot00000000000000__author__ = "Patrick Lehmann" __email__ = "Paebbels@gmail.com" __copyright__ = "2017-2026, Patrick Lehmann" __license__ = "Apache License, Version 2.0" __version__ = "0.1.0" __keywords__ = ["Dummy"] __issue_tracker__ = "https://GitHub.com/pyTooling/pyTooling/issues" pyTooling-8.11.0/tests/pyPackage/000077500000000000000000000000001513317154500166405ustar00rootroot00000000000000pyTooling-8.11.0/tests/pyPackage/README.ascii000066400000000000000000000000121513317154500206000ustar00rootroot00000000000000pyPackage pyTooling-8.11.0/tests/pyPackage/README.md000066400000000000000000000000141513317154500201120ustar00rootroot00000000000000# pyPackage pyTooling-8.11.0/tests/pyPackage/README.rst000066400000000000000000000000241513317154500203230ustar00rootroot00000000000000pyPackage ######### pyTooling-8.11.0/tests/pyPackage/README.txt000066400000000000000000000000151513317154500203320ustar00rootroot000000000000001. pyPackage pyTooling-8.11.0/tests/pyPackage/__init__.py000066400000000000000000000004601513317154500207510ustar00rootroot00000000000000__author__ = "Patrick Lehmann" __email__ = "Paebbels@gmail.com" __copyright__ = "2017-2026, Patrick Lehmann" __license__ = "Apache License, Version 2.0" __version__ = "0.1.0" __keywords__ = ["Dummy"] __issue_tracker__ = "https://GitHub.com/pyTooling/pyTooling/issues" pyTooling-8.11.0/tests/pyPackage/requirements.Build.txt000066400000000000000000000000001513317154500231500ustar00rootroot00000000000000pyTooling-8.11.0/tests/pyPackage/requirements.Dist.txt000066400000000000000000000000001513317154500230140ustar00rootroot00000000000000pyTooling-8.11.0/tests/pyPackage/requirements.Doc.txt000066400000000000000000000000001513317154500226160ustar00rootroot00000000000000pyTooling-8.11.0/tests/pyPackage/requirements.Test.txt000066400000000000000000000000001513317154500230300ustar00rootroot00000000000000pyTooling-8.11.0/tests/pyPackage/requirements.txt000066400000000000000000000000001513317154500221120ustar00rootroot00000000000000pyTooling-8.11.0/tests/requirements.txt000066400000000000000000000001621513317154500202170ustar00rootroot00000000000000-r benchmark/requirements.txt -r performance/requirements.txt -r typing/requirements.txt -r unit/requirements.txt pyTooling-8.11.0/tests/typing/000077500000000000000000000000001513317154500162465ustar00rootroot00000000000000pyTooling-8.11.0/tests/typing/requirements.txt000066400000000000000000000004361513317154500215350ustar00rootroot00000000000000-r ../../requirements.txt # Static Type Checking mypy[reports] ~= 1.19 typing_extensions ~= 4.15 types-colorama >= 0.4.15 types-setuptools >= 80.9 lxml >= 5.4, <7.0 # For pyTooling.Configuration.YAML testing ruamel.yaml ~= 0.18.0 # For pyTooling.TerminalUI testing colorama ~= 0.4.6 pyTooling-8.11.0/tests/unit/000077500000000000000000000000001513317154500157135ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Attributes/000077500000000000000000000000001513317154500200415ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Attributes/ArgParse.py000066400000000000000000001316251513317154500221270ustar00rootroot00000000000000# ==================================================================================================================== # # _ _ _ _ _ _ _ ____ # # / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ # # / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ # # _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ # # (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| # # |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for argparse attributes. """ from argparse import ArgumentError from io import StringIO from pathlib import Path from typing import Callable, Any, Tuple, NoReturn from unittest import TestCase from unittest.mock import patch from pyTooling.Attributes.ArgParse import ArgParseHelperMixin, DefaultHandler, CommandHandler, CommandLineArgument from pyTooling.Attributes.ArgParse.Argument import StringArgument, StringListArgument, PositionalArgument from pyTooling.Attributes.ArgParse.Argument import PathArgument, PathListArgument, ListArgument from pyTooling.Attributes.ArgParse.Argument import IntegerArgument, IntegerListArgument from pyTooling.Attributes.ArgParse.Argument import FloatArgument, FloatListArgument from pyTooling.Attributes.ArgParse.Flag import FlagArgument, ShortFlag, LongFlag from pyTooling.Attributes.ArgParse.ValuedFlag import ValuedFlag, ShortValuedFlag, LongValuedFlag from pyTooling.Attributes.ArgParse.KeyValueFlag import NamedKeyValuePairsArgument, ShortKeyValueFlag, LongKeyValueFlag from pyTooling.TerminalUI import TerminalApplication if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class ProgramBase(ArgParseHelperMixin, mixin=True): handler: Callable args: Any def __init__(self) -> None: super().__init__(prog="Program.py") class Common(TestCase): def test_NoArgs(self) -> None: class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() arguments = [] parsed = prog.MainParser.parse_args(arguments) prog._RouteToHandler(parsed) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 0, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback def test_DefaultHelpShort(self) -> None: print() class Program(ProgramBase, ArgParseHelperMixin): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() arguments = ["-h"] with self.assertRaises(SystemExit) as ex: parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) self.assertEqual(0, ex.exception.code) def test_DefaultHelpLong(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() arguments = ["--help"] with self.assertRaises(SystemExit) as ex: parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) self.assertEqual(0, ex.exception.code) def test_Help(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("help", help="Display help page(s) for the given command name.") @CommandLineArgument(metavar="Command", dest="Command", type=str, nargs="?", help="Print help page(s) for a command.") def HandleHelp(self, args) -> None: self.handler = self.HandleHelp self.args = args prog = Program() arguments = ["help"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleHelp) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback def test_HelpPlusArgWithoutArg(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("help", help="Display help page(s) for the given command name.") @CommandLineArgument(metavar="Command", dest="Command", type=str, nargs="?", help="Print help page(s) for a command.") def HandleHelp(self, args) -> None: self.handler = self.HandleHelp self.args = args prog = Program() arguments = ["help"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleHelp) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.Command) def test_HelpPlusArgWithArg(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("help", help="Display help page(s) for the given command name.") @CommandLineArgument(metavar="Command", dest="Command", type=str, nargs="?", help="Print help page(s) for a command.") def HandleHelp(self, args) -> None: self.handler = self.HandleHelp self.args = args prog = Program() arguments = ["help", "info"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleHelp) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("info", prog.args.Command) def test_HelpShort(self) -> None: print() class Program(ProgramBase): @DefaultHandler() # -h and --help causes an ArgumentError: argument -h/--help: conflicting option strings: -h, --help # @ArgumentAttribute("-h", "--help", dest="help", action="store_const", const=True, default=False, help="Show help.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() arguments = ["-h"] with self.assertRaises(SystemExit) as ex: parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) self.assertEqual(0, ex.exception.code) # An internal help page is printed and the parser causes a SystemExit exception # self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") def test_HelpLong(self) -> None: print() class Program(ProgramBase): @DefaultHandler() # -h and --help causes an ArgumentError: argument -h/--help: conflicting option strings: -h, --help # @ArgumentAttribute("-h", "--help", dest="help", action="store_const", const=True, default=False, help="Show help.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() arguments = ["--help"] with self.assertRaises(SystemExit) as ex: parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) self.assertEqual(0, ex.exception.code) # An internal help page is printed and the parser causes a SystemExit exception # self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") def test_VerboseShortWithoutV(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @CommandLineArgument("-v", "--verbose", dest="verbose", action="store_const", const=True, default=False, help="Show verbose messages.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() arguments = [] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) def test_VerboseShortWithV(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @CommandLineArgument("-v", "--verbose", dest="verbose", action="store_const", const=True, default=False, help="Show verbose messages.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() arguments = ["-v"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) class Commands(TestCase): def test_TwoCommands(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @FlagArgument(short="-v", long="--verbose", dest="verbose", help="Show verbose messages.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("cmd1", help="First command.") def Cmd1Handler(self, args) -> None: self.handler = self.Cmd1Handler self.args = args @CommandHandler("cmd2", help="First command.") def Cmd2Handler(self, args) -> None: self.handler = self.Cmd2Handler self.args = args prog = Program() # Checking cmd1 command arguments = ["cmd1"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.Cmd1Handler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback # Checking cmd2 command arguments = ["cmd2"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.Cmd2Handler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback # Checking wrong command arguments = ["cmd3"] with self.assertRaises(ArgumentError) as ctx: parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) self.assertIn("invalid choice", ctx.exception.message) self.assertIn("cmd3", ctx.exception.message) # Checking missing command arguments = [] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) self.assertFalse(parsed.verbose) self.assertEqual(0, len(nonProcessedArgs)) class Values(TestCase): def test_Positional(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @PositionalArgument(dest="username", metaName="username", type=str, help="Name of the user.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter arguments = ["paebbels"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("paebbels", prog.args.username) def test_String(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @StringArgument(dest="username", metaName="username", help="Name of the user.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter arguments = ["paebbels"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("paebbels", prog.args.username) def test_Integer(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @IntegerArgument(dest="count", metaName="count", help="Number of users.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter arguments = ["24"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(24, prog.args.count) def test_Float(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @FloatArgument(dest="maxVoltage", metaName="max voltage", help="Maximum allowed voltage.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter arguments = ["12.4"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(12.4, prog.args.maxVoltage) def test_Path(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @PathArgument(dest="configFile", metaName="path", help="Configuration file") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking path parameter path = Path("../etc/config.json") arguments = [path.as_posix()] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(path, prog.args.configFile) class ValueLists(TestCase): def test_Lists(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @ListArgument(dest="usernames", metaName="usernames", type=str, help="Names of the users.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking list of usernames lst = ["paebbels", "paebbelslemmi"] arguments = lst.copy() parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertListEqual(lst, prog.args.usernames) def test_StringList(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @StringListArgument(dest="usernames", metaName="usernames", help="Names of the users.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking list of usernames lst = ["paebbels", "paebbelslemmi"] arguments = lst.copy() parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertListEqual(lst, prog.args.usernames) def test_IntegerList(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @IntegerListArgument(dest="counts", metaName="counts", help="Numbers of users.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter lst = [24, 2, 86] arguments = [str(e) for e in lst] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertListEqual(lst, prog.args.counts) def test_FloatList(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @FloatListArgument(dest="maxVoltages", metaName="max voltages", help="Maximum allowed voltages.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter lst = [3.4, 5.5, 13.2] arguments = [str(e) for e in lst] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertListEqual(lst, prog.args.maxVoltages) def test_PathList(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @PathListArgument(dest="configFiles", metaName="paths", help="Configuration files.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking path parameter path1 = Path("../etc/config.json") path2 = Path("../etc/daemon.toml") lst = [path1, path2] arguments = [p.as_posix() for p in lst] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertListEqual(lst, prog.args.configFiles) class Flags(TestCase): def test_DefaultHandler_ShortAndLong(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @FlagArgument(short="-v", long="--verbose", dest="verbose", help="Show verbose messages.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter arguments = ["-v"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) # Checking wrong parameter arguments = ["-V"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments, nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) # Checking long parameter arguments = ["--verbose"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) # Checking wrong parameter arguments = ["--ver"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments, nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) def test_DefaultHandler_Short(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @ShortFlag("-v", dest="verbose", help="Show verbose messages.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter arguments = ["-v"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) # Checking wrong parameter arguments = ["-V"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments, nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) def test_DefaultHandler_Long(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @LongFlag("--verbose", dest="verbose", help="Show verbose messages.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking long parameter arguments = ["--verbose"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) # Checking wrong parameter arguments = ["--ver"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments, nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) def test_CommandHandler_ShortAndLong(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("cmd", help="Command") @FlagArgument(short="-v", long="--verbose", dest="verbose", help="Show verbose messages.") def CmdHandler(self, args) -> None: self.handler = self.CmdHandler self.args = args prog = Program() # Checking short parameter arguments = ["cmd", "-v"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) # Checking wrong parameter arguments = ["cmd", "-V"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments[1:], nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) # Checking long parameter arguments = ["cmd", "--verbose"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) # Checking wrong parameter arguments = ["cmd", "--ver"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments[1:], nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) def test_CommandHandler_Short(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("cmd", help="Command") @ShortFlag("-v", dest="verbose", help="Show verbose messages.") def CmdHandler(self, args) -> None: self.handler = self.CmdHandler self.args = args prog = Program() # Checking short parameter arguments = ["cmd", "-v"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) # Checking wrong parameter arguments = ["cmd", "-V"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments[1:], nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) def test_CommandHandler_Long(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("cmd", help="Command") @LongFlag("--verbose", dest="verbose", help="Show verbose messages.") def CmdHandler(self, args) -> None: self.handler = self.CmdHandler self.args = args prog = Program() # Checking long parameter arguments = ["cmd", "--verbose"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(True, prog.args.verbose) # Checking wrong parameter arguments = ["cmd", "--ver"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments[1:], nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual(False, prog.args.verbose) class ValuedFlags(TestCase): def test_DefaultHandler_ShortAndLong(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @ValuedFlag(short="-c", long="--count", dest="count", optional=True, help="Number of elements.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter arguments = ["-c=1"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("1", prog.args.count) # Checking wrong parameter arguments = ["-C=2"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments, nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.count) # Checking long parameter arguments = ["--count=3"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("3", prog.args.count) # Checking wrong parameter arguments = ["--cnt=4"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments, nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.count) def test_DefaultHandler_Short(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @ShortValuedFlag("-c", dest="count", optional=True, help="Number of elements.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking short parameter arguments = ["-c=5"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("5", prog.args.count) # Checking wrong parameter arguments = ["-C=6"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments, nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.count) def test_DefaultHandler_Long(self) -> None: print() class Program(ProgramBase): @DefaultHandler() @LongValuedFlag("--count", dest="count", optional=True, help="Number of elements.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args prog = Program() # Checking long parameter arguments = ["--count=7"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("7", prog.args.count) # Checking wrong parameter arguments = ["--cnt=8"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments, nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.HandleDefault) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.count) def test_CommandHandler_ShortAndLong(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("cmd", help="Command") @ValuedFlag(short="-c", long="--count", dest="count", optional=True, help="Number of elements.") def CmdHandler(self, args) -> None: self.handler = self.CmdHandler self.args = args prog = Program() # Checking short parameter arguments = ["cmd", "-c=11"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("11", prog.args.count) # Checking wrong parameter arguments = ["cmd", "-C=12"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments[1:], nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.count) # Checking long parameter arguments = ["cmd", "--count=13"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("13", prog.args.count) # Checking wrong parameter arguments = ["cmd", "--cnt=14"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments[1:], nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.count) def test_CommandHandler_Short(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("cmd", help="Command") @ShortValuedFlag("-c", dest="count", optional=True, help="Number of elements.") def CmdHandler(self, args) -> None: self.handler = self.CmdHandler self.args = args prog = Program() # Checking short parameter arguments = ["cmd", "-c=15"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("15", prog.args.count) # Checking wrong parameter arguments = ["cmd", "-C=16"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments[1:], nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.count) def test_CommandHandler_Long(self) -> None: print() class Program(ProgramBase): @DefaultHandler() def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("cmd", help="Command") @LongValuedFlag("--count", dest="count", optional=True, help="Number of elements.") def CmdHandler(self, args) -> None: self.handler = self.CmdHandler self.args = args prog = Program() # Checking long parameter arguments = ["cmd", "--count=17"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("17", prog.args.count) # Checking wrong parameter arguments = ["cmd", "--cnt=18"] parsed, nonProcessedArgs = prog.MainParser.parse_known_args(arguments) prog._RouteToHandler(parsed) self.assertEqual(1, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertListEqual(arguments[1:], nonProcessedArgs) self.assertIs(prog.handler.__func__, Program.CmdHandler) self.assertEqual(1 + 1, len(prog.args.__dict__), f"args: {prog.args.__dict__}") #: 1+ for 'func' as callback self.assertIsNone(prog.args.count) class Program(ProgramBase, mixin=True): @DefaultHandler() @FlagArgument(short="-v", long="--verbose", dest="verbose", help="Show verbose messages.") def HandleDefault(self, args) -> None: self.handler = self.HandleDefault self.args = args @CommandHandler("new-user", help="Add a new user.") @StringArgument(dest="username", metaName="username", help="Name of the new user.") @LongValuedFlag("--quota", dest="quota", help="Max usable disk space.") def NewUserHandler(self, args) -> None: self.handler = self.NewUserHandler self.args = args @CommandHandler("delete-user", help="Delete a user.") @StringArgument(dest="username", metaName="username", help="Name of the new user.") @FlagArgument(short="-f", long="--force", dest="force", help="Ignore internal checks.") def DeleteUserHandler(self, args) -> None: self.handler = self.DeleteUserHandler self.args = args @CommandHandler("list-user", help="Add a new user.") def ListUserHandler(self, args) -> None: self.handler = self.ListUserHandler self.args = args class UserManager(TestCase): def test_UserManager(self) -> None: print() class Application(Program): pass app = Application() # Checking long parameter arguments = ["new-user", "username", "--quota=17"] parsed, nonProcessedArgs = app.MainParser.parse_known_args(arguments) app._RouteToHandler(parsed) self.assertEqual(0, len(nonProcessedArgs), f"Remaining options: {nonProcessedArgs}") self.assertIs(app.handler.__func__, Program.NewUserHandler) self.assertEqual(3 + 1, len(app.args.__dict__), f"args: {app.args.__dict__}") #: 1+ for 'func' as callback self.assertEqual("17", app.args.quota) class Application(TerminalApplication, Program): HeadLine = "Application" def __init__(self) -> None: super().__init__() Program.__init__(self) def Run(self) -> NoReturn: returnCode = 0 try: super().Run() # todo: enableAutoComplete ?? except ArgumentError as ex: self._PrintHeadline() self.WriteError(ex.message) returnCode = 2 self.Exit(returnCode) class MockedUserManager(TestCase): @staticmethod def _PrintToStdOutAndStdErr(out: StringIO, err: StringIO, stdoutEnd: str = "") -> Tuple[str, str]: out.seek(0) err.seek(0) stdout = out.read() stderr = err.read() print("-- STDOUT " + "-" * 70) print(stdout, end=stdoutEnd) if len(stderr) > 0: print("-- STDERR " + "-" * 70) print(stderr, end="") print("-" * 80) return stdout, stderr @patch("sys.argv", ["help", "expand"]) def test_HelpForExport(self) -> None: print() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() with self.assertRaises(SystemExit) as ctx: app.Run() stdout, stderr = self._PrintToStdOutAndStdErr(out, err) self.assertEqual(2, ctx.exception.code) self.assertIn("Application", stdout) # self.assertIn(f"usage: {PROGRAM}", stdout) # self.assertIn(f"usage: {PROGRAM}", stderr) self.assertEqual("", stderr) pyTooling-8.11.0/tests/unit/Attributes/AttributesOnClasses.py000066400000000000000000000230521513317154500243560ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for attributes attached to methods. """ from unittest import TestCase from pyTooling.MetaClasses import ExtendedType from pytest import mark from pyTooling.Attributes import Attribute if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class ApplyClassAttributes(TestCase): def test_SingleClass(self) -> None: class AttributeA(Attribute): pass @AttributeA() class Class1: pass foundClasses = [c for c in AttributeA.GetClasses()] self.assertListEqual(foundClasses, [Class1]) def test_MultipleClasses(self) -> None: class AttributeA(Attribute): pass @AttributeA() class Class1: pass @AttributeA() class Class2: pass foundClasses = [c for c in AttributeA.GetClasses()] self.assertListEqual(foundClasses, [Class1, Class2]) def test_MultipleAttributes(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass @AttributeA() @AttributeB() class Class1: pass foundClassesForA = [c for c in AttributeA.GetClasses()] foundClassesForB = [c for c in AttributeB.GetClasses()] self.assertListEqual(foundClassesForA, [Class1]) self.assertListEqual(foundClassesForB, [Class1]) def test_MultipleClassesAndAttributes(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass @AttributeA() class Class1: pass @AttributeA() @AttributeB() class Class2: pass @AttributeB() @AttributeA() class Class3: pass @AttributeB() class Class4: pass foundClassesForA = [c for c in AttributeA.GetClasses()] foundClassesForB = [c for c in AttributeB.GetClasses()] self.assertListEqual(foundClassesForA, [Class1, Class2, Class3]) self.assertListEqual(foundClassesForB, [Class2, Class3, Class4]) def test_DerivedAttributes(self) -> None: class AttributeA1(Attribute): pass class AttributeA2(AttributeA1): pass @AttributeA1() class Class1: pass @AttributeA2() class Class2: pass foundClassesForA1 = [c for c in AttributeA1.GetClasses()] foundClassesForA2 = [c for c in AttributeA2.GetClasses()] self.assertListEqual(foundClassesForA1, [Class1]) self.assertListEqual(foundClassesForA2, [Class2]) def test_DoubleAppliedAttribute(self) -> None: class AttributeA(Attribute): pass @AttributeA() @AttributeA() class Class1: pass foundClasses = [c for c in AttributeA.GetClasses()] self.assertListEqual(foundClasses, [Class1, Class1]) def test_AttributeAndDerivedAttribute(self) -> None: class AttributeA1(Attribute): pass class AttributeA2(AttributeA1): pass @AttributeA1() @AttributeA2() class Class1: pass foundClassesForA1 = [c for c in AttributeA1.GetClasses()] foundClassesForA2 = [c for c in AttributeA2.GetClasses()] self.assertListEqual(foundClassesForA1, [Class1]) self.assertListEqual(foundClassesForA2, [Class1]) class ModuleAttribute(Attribute): pass class GlobalAttribute(Attribute): pass @ModuleAttribute() @GlobalAttribute() class ModuleClass: pass @ModuleAttribute() @GlobalAttribute() class InnerClass: pass class Filtering(TestCase): def test_SubclassOf(self) -> None: class AttributeA(Attribute): pass @AttributeA() class Class1(metaclass=ExtendedType): pass class Class2(Class1): pass class Class3(Class2): pass foundClasses = [c for c in AttributeA.GetClasses()] foundClassesForC1 = [c for c in AttributeA.GetClasses(subclassOf=Class1)] foundClassesForC2 = [c for c in AttributeA.GetClasses(subclassOf=Class2)] foundClassesForC3 = [c for c in AttributeA.GetClasses(subclassOf=Class3)] self.assertListEqual(foundClasses, [Class1, Class2, Class3]) self.assertListEqual(foundClassesForC1, [Class1, Class2, Class3]) self.assertListEqual(foundClassesForC2, [Class2, Class3]) self.assertListEqual(foundClassesForC3, [Class3]) def test_Scope_Module(self) -> None: from sys import modules foundClasses = [c for c in ModuleAttribute.GetClasses()] foundModuleClasses = [c for c in ModuleAttribute.GetClasses(scope=modules[ModuleClass.__module__])] self.assertListEqual(foundClasses, [ModuleClass.InnerClass, ModuleClass]) self.assertListEqual(foundModuleClasses, [ModuleClass]) @mark.skip(reason="Unclear how to get a local scope object.") def test_Scope_Local(self) -> None: class LocalAttribute(Attribute): pass @LocalAttribute() class LocalClass: pass @LocalAttribute() class NestedClass: pass l = locals() foundClasses = [c for c in LocalAttribute.GetClasses()] foundLocalClasses = [c for c in LocalAttribute.GetClasses(scope=l)] self.assertListEqual(foundClasses, [LocalClass.NestedClass, LocalClass]) self.assertListEqual(foundLocalClasses, [LocalClass]) def test_Scope_Nested(self) -> None: @GlobalAttribute() class LocalClass: pass @GlobalAttribute() class NestedClass: pass l = locals() foundClasses = [c for c in GlobalAttribute.GetClasses()] foundInnerClasses = [c for c in GlobalAttribute.GetClasses(scope=ModuleClass)] foundNestedClasses = [c for c in GlobalAttribute.GetClasses(scope=LocalClass)] self.assertListEqual(foundClasses, [ModuleClass.InnerClass, ModuleClass, LocalClass.NestedClass, LocalClass]) self.assertListEqual(foundInnerClasses, [ModuleClass.InnerClass]) self.assertListEqual(foundNestedClasses, [LocalClass.NestedClass]) def test_Scope_SubclassOf_Nested(self) -> None: class LocalAttribute(Attribute): pass class Base: pass @LocalAttribute() class LocalClass: pass @LocalAttribute() class NestedClass1: pass @LocalAttribute() class NestedClass2(Base): pass foundClasses = [c for c in LocalAttribute.GetClasses()] foundNestedClasses = [c for c in LocalAttribute.GetClasses(scope=LocalClass, subclassOf=Base)] self.assertListEqual(foundClasses, [LocalClass.NestedClass1, LocalClass.NestedClass2, LocalClass]) self.assertListEqual(foundNestedClasses, [LocalClass.NestedClass2]) pyTooling-8.11.0/tests/unit/Attributes/AttributesOnFunctions.py000066400000000000000000000141011513317154500247240ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for attributes attached to methods. """ from unittest import TestCase from pytest import mark from pyTooling.Attributes import Attribute if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class ApplyFunctionAttributes(TestCase): def test_SingleFunction(self) -> None: class AttributeA(Attribute): pass @AttributeA() def func1(): pass foundFunctions = [c for c in AttributeA.GetFunctions()] self.assertListEqual(foundFunctions, [func1]) def test_MultipleFunctions(self) -> None: class AttributeA(Attribute): pass @AttributeA() def func1(): pass @AttributeA() def func2(): pass foundFunctions = [c for c in AttributeA.GetFunctions()] self.assertListEqual(foundFunctions, [func1, func2]) class ModuleAttribute(Attribute): pass class GlobalAttribute(Attribute): pass @ModuleAttribute() @GlobalAttribute() def ModuleFunction() -> None: pass @GlobalAttribute() def NestedFunction() -> None: pass return NestedFunction moduleNestedFunction = ModuleFunction() class Filtering(TestCase): def test_Scope_Module(self) -> None: from sys import modules foundFunctions = [c for c in ModuleAttribute.GetFunctions()] foundModuleFunctions = [c for c in ModuleAttribute.GetFunctions(scope=modules[ModuleFunction.__module__])] self.assertListEqual(foundFunctions, [ModuleFunction]) self.assertListEqual(foundModuleFunctions, [ModuleFunction]) @mark.skip(reason="Unclear how to get a local scope object.") def test_Scope_Local(self) -> None: class LocalAttribute(Attribute): pass @LocalAttribute() def LocalFunction(): pass @LocalAttribute() def NestedFunction(): pass return NestedFunction nestedFunction = LocalFunction() # l = locals() foundFunctions = [c for c in LocalAttribute.GetFunctions()] # foundLocalClasses = [c for c in LocalAttribute.GetClasses(scope=l)] self.assertListEqual(foundFunctions, [nestedFunction, LocalFunction]) # self.assertListEqual(foundLocalClasses, [LocalClass]) def test_Scope_Nested(self) -> None: @GlobalAttribute() def LocalFunction(): pass @GlobalAttribute() def NestedFunction(): pass return NestedFunction nestedFunction = LocalFunction() l = locals() foundFunctions = [c for c in GlobalAttribute.GetFunctions()] self.assertListEqual(foundFunctions, [ModuleFunction, moduleNestedFunction, LocalFunction, nestedFunction]) pyTooling-8.11.0/tests/unit/Attributes/AttributesOnMethods.py000066400000000000000000001227211513317154500243670ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for attributes attached to methods. """ from unittest import TestCase from pytest import mark from pyTooling.MetaClasses import ExtendedType from pyTooling.Attributes import Attribute if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class ApplyMethodAttributes_NoMetaClass(TestCase): def test_NoAttribute(self) -> None: class AttributeA(Attribute): pass class Class1: def meth0(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth0)] self.assertEqual(0, len(foundMethodsOnAttributeA)) self.assertEqual(0, len(foundAttributesAOnClass1Meth1)) def test_SingleAttribute_SingleClass_SingleMethod(self) -> None: class AttributeA(Attribute): pass class Class1: def meth0(self): pass @AttributeA() def meth1(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundAttributesAOnClass1Meth0 = [a for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth1)] self.assertEqual(1, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) for a in foundAttributesAOnClass1Meth1: self.assertIsInstance(a, AttributeA) def test_SingleAttribute_SingleClass_MultipleMethods(self) -> None: class AttributeA(Attribute): pass class Class1: def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeA() def meth2(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundAttributesAOnClass1Meth0 = [a for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [a for a in AttributeA.GetAttributes(Class1.meth2)] self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth2]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) for a in foundAttributesAOnClass1Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) for a in foundAttributesAOnClass1Meth2: self.assertIsInstance(a, AttributeA) def test_SingleAttribute_MultipleClasses_SingleMethod(self) -> None: class AttributeA(Attribute): pass class Class1: def meth0(self): pass @AttributeA() def meth1(self): pass class Class2: def meth0(self): pass @AttributeA() def meth1(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundAttributesAOnClass1Meth0 = [a for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass2Meth0 = [a for a in AttributeA.GetAttributes(Class2.meth0)] foundAttributesAOnClass2Meth1 = [a for a in AttributeA.GetAttributes(Class2.meth1)] self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class2.meth1]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) for a in foundAttributesAOnClass1Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(0, len(foundAttributesAOnClass2Meth0)) self.assertEqual(1, len(foundAttributesAOnClass2Meth1)) for a in foundAttributesAOnClass2Meth1: self.assertIsInstance(a, AttributeA) def test_SingleAttribute_MultipleClasses_MultipleMethods(self) -> None: class AttributeA(Attribute): pass class Class1: def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeA() def meth2(self): pass class Class2: @AttributeA() def meth1(self): pass def meth2(self): pass @AttributeA() def meth3(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundAttributesAOnClass1Meth0 = [a for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [a for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass2Meth1 = [a for a in AttributeA.GetAttributes(Class2.meth1)] foundAttributesAOnClass2Meth2 = [a for a in AttributeA.GetAttributes(Class2.meth2)] foundAttributesAOnClass2Meth3 = [a for a in AttributeA.GetAttributes(Class2.meth3)] self.assertEqual(4, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth2, Class2.meth1, Class2.meth3]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) for a in foundAttributesAOnClass1Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) for a in foundAttributesAOnClass1Meth2: self.assertIsInstance(a, AttributeA) self.assertEqual(1, len(foundAttributesAOnClass2Meth1)) for a in foundAttributesAOnClass2Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(0, len(foundAttributesAOnClass2Meth2)) self.assertEqual(1, len(foundAttributesAOnClass2Meth3)) for a in foundAttributesAOnClass2Meth3: self.assertIsInstance(a, AttributeA) def test_MultipleAttributes_SingleClass_SingleMethod(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1: def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass @AttributeB() def meth2(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeB = [f for f in AttributeB.GetFunctions()] foundAttributesAOnClass1Meth0 = [a for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [a for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesBOnClass1Meth1 = [b for b in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [b for b in AttributeB.GetAttributes(Class1.meth2)] foundAttributesBOnClass1Meth3 = [b for b in AttributeB.GetAttributes(Class1.meth0)] self.assertEqual(1, len(foundMethodsOnAttributeA)) self.assertEqual(2, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1]) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth1, Class1.meth2]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) for a in foundAttributesAOnClass1Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(0, len(foundAttributesAOnClass1Meth2)) self.assertEqual(1, len(foundAttributesBOnClass1Meth1)) for b in foundAttributesBOnClass1Meth1: self.assertIsInstance(b, AttributeB) self.assertEqual(1, len(foundAttributesBOnClass1Meth2)) for b in foundAttributesBOnClass1Meth2: self.assertIsInstance(b, AttributeB) self.assertEqual(0, len(foundAttributesBOnClass1Meth3)) def test_MultipleAttributes_SingleClass_MultipleMethods(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1: def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass @AttributeA() def meth2(self): pass @AttributeB() def meth3(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeB = [f for f in AttributeB.GetFunctions()] foundAttributesAOnClass1Meth0 = [a for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [a for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass1Meth3 = [a for a in AttributeA.GetAttributes(Class1.meth3)] foundAttributesBOnClass1Meth0 = [b for b in AttributeB.GetAttributes(Class1.meth0)] foundAttributesBOnClass1Meth1 = [b for b in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [b for b in AttributeB.GetAttributes(Class1.meth2)] foundAttributesBOnClass1Meth3 = [b for b in AttributeB.GetAttributes(Class1.meth3)] self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertEqual(2, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth2]) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth1, Class1.meth3]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) for a in foundAttributesAOnClass1Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) for a in foundAttributesAOnClass1Meth2: self.assertIsInstance(a, AttributeA) self.assertEqual(0, len(foundAttributesAOnClass1Meth3)) self.assertEqual(0, len(foundAttributesBOnClass1Meth0)) self.assertEqual(1, len(foundAttributesBOnClass1Meth1)) for b in foundAttributesBOnClass1Meth1: self.assertIsInstance(b, AttributeB) self.assertEqual(0, len(foundAttributesBOnClass1Meth2)) self.assertEqual(1, len(foundAttributesBOnClass1Meth3)) for b in foundAttributesBOnClass1Meth3: self.assertIsInstance(b, AttributeB) def test_MultipleAttributes_MultipleClasses_SingleMethod(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1: def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass @AttributeB() def meth2(self): pass class Class2: def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeB = [f for f in AttributeB.GetFunctions()] foundAttributesAOnClass1Meth0 = [a for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [a for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass2Meth0 = [a for a in AttributeA.GetAttributes(Class2.meth0)] foundAttributesAOnClass2Meth1 = [a for a in AttributeA.GetAttributes(Class2.meth1)] foundAttributesBOnClass1Meth0 = [b for b in AttributeB.GetAttributes(Class1.meth0)] foundAttributesBOnClass1Meth1 = [b for b in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [b for b in AttributeB.GetAttributes(Class1.meth2)] foundAttributesBOnClass2Meth0 = [b for b in AttributeB.GetAttributes(Class2.meth0)] foundAttributesBOnClass2Meth1 = [b for b in AttributeB.GetAttributes(Class2.meth1)] self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertEqual(3, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class2.meth1]) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth1, Class1.meth2, Class2.meth1]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) for a in foundAttributesAOnClass1Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(0, len(foundAttributesAOnClass1Meth2)) self.assertEqual(0, len(foundAttributesAOnClass2Meth0)) self.assertEqual(1, len(foundAttributesAOnClass2Meth1)) for a in foundAttributesAOnClass2Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(0, len(foundAttributesBOnClass1Meth0)) self.assertEqual(1, len(foundAttributesBOnClass1Meth1)) for b in foundAttributesBOnClass1Meth1: self.assertIsInstance(b, AttributeB) self.assertEqual(1, len(foundAttributesBOnClass1Meth2)) for b in foundAttributesBOnClass1Meth2: self.assertIsInstance(b, AttributeB) self.assertEqual(0, len(foundAttributesBOnClass2Meth0)) self.assertEqual(1, len(foundAttributesBOnClass2Meth1)) for b in foundAttributesBOnClass2Meth1: self.assertIsInstance(b, AttributeB) def test_MultipleAttributes_MultipleClasses_MultipleMethods(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1: def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass @AttributeA() def meth2(self): pass class Class2: def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeA() def meth2(self): pass @AttributeA() @AttributeB() def meth3(self): pass foundMethodsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeB = [f for f in AttributeB.GetFunctions()] foundAttributesAOnClass1Meth0 = [a for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [a for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [a for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass2Meth0 = [a for a in AttributeA.GetAttributes(Class2.meth0)] foundAttributesAOnClass2Meth1 = [a for a in AttributeA.GetAttributes(Class2.meth1)] foundAttributesAOnClass2Meth2 = [a for a in AttributeA.GetAttributes(Class2.meth2)] foundAttributesAOnClass2Meth3 = [a for a in AttributeA.GetAttributes(Class2.meth3)] foundAttributesBOnClass1Meth0 = [a for a in AttributeB.GetAttributes(Class1.meth0)] foundAttributesBOnClass1Meth1 = [a for a in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [a for a in AttributeB.GetAttributes(Class1.meth2)] foundAttributesBOnClass2Meth0 = [a for a in AttributeB.GetAttributes(Class2.meth0)] foundAttributesBOnClass2Meth1 = [a for a in AttributeB.GetAttributes(Class2.meth1)] foundAttributesBOnClass2Meth2 = [a for a in AttributeB.GetAttributes(Class2.meth2)] foundAttributesBOnClass2Meth3 = [a for a in AttributeB.GetAttributes(Class2.meth3)] self.assertEqual(5, len(foundMethodsOnAttributeA)) self.assertEqual(2, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth2, Class2.meth1, Class2.meth2, Class2.meth3]) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth1, Class2.meth3]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) for a in foundAttributesAOnClass1Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) for a in foundAttributesAOnClass1Meth2: self.assertIsInstance(a, AttributeA) self.assertEqual(0, len(foundAttributesAOnClass2Meth0)) self.assertEqual(1, len(foundAttributesAOnClass2Meth1)) for a in foundAttributesAOnClass2Meth1: self.assertIsInstance(a, AttributeA) self.assertEqual(1, len(foundAttributesAOnClass2Meth2)) for a in foundAttributesAOnClass2Meth2: self.assertIsInstance(a, AttributeA) self.assertEqual(1, len(foundAttributesAOnClass2Meth3)) for a in foundAttributesAOnClass2Meth3: self.assertIsInstance(a, AttributeA) self.assertEqual(0, len(foundAttributesBOnClass1Meth0)) self.assertEqual(1, len(foundAttributesBOnClass1Meth1)) for b in foundAttributesBOnClass1Meth1: self.assertIsInstance(b, AttributeB) self.assertEqual(0, len(foundAttributesBOnClass1Meth2)) self.assertEqual(0, len(foundAttributesBOnClass2Meth0)) self.assertEqual(0, len(foundAttributesBOnClass2Meth1)) self.assertEqual(0, len(foundAttributesBOnClass2Meth2)) self.assertEqual(1, len(foundAttributesBOnClass2Meth3)) for b in foundAttributesBOnClass2Meth3: self.assertIsInstance(b, AttributeB) class ApplyMethodAttributes_WithMetaClass(TestCase): def test_NoAttribute(self) -> None: class AttributeA(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundMethodsOnClass1 = [m for m in Class1.GetMethodsWithAttributes()] # foundAttributesOnClass1Meth1 = Class1.GetAttributes(Class1.meth0) self.assertFalse(Class1.HasClassAttributes) self.assertFalse(Class1.HasMethodAttributes) self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(0, len(foundMethodsOnAttributeA)) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) # self.assertFalse(Class1.HasAttributes()) # self.assertFalse(Class1.HasAttribute(Class1.meth0)) self.assertEqual(0, len(foundMethodsOnClass1)) # self.assertEqual(0, len(foundAttributesOnClass1Meth1)) def test_SingleAttribute_SingleClass_SingleMethod(self) -> None: class AttributeA(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(1, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) def test_SingleAttribute_SingleClass_MultipleMethods(self) -> None: class AttributeA(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeA() def meth2(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [type(a) for a in AttributeA.GetAttributes(Class1.meth2)] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth2]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) self.assertListEqual(foundAttributesAOnClass1Meth2, [AttributeA]) def test_SingleAttribute_MultipleClasses_SingleMethod(self) -> None: class AttributeA(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass class Class2(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass2Meth0 = [type(a) for a in AttributeA.GetAttributes(Class2.meth0)] foundAttributesAOnClass2Meth1 = [type(a) for a in AttributeA.GetAttributes(Class2.meth1)] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class2.meth1]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) self.assertEqual(0, len(foundAttributesAOnClass2Meth0)) self.assertEqual(1, len(foundAttributesAOnClass2Meth1)) self.assertListEqual(foundAttributesAOnClass2Meth1, [AttributeA]) def test_SingleAttribute_MultipleClasses_MultipleMethods(self) -> None: class AttributeA(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeA() def meth2(self): pass class Class2(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeA() def meth2(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [type(a) for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass2Meth0 = [type(a) for a in AttributeA.GetAttributes(Class2.meth0)] foundAttributesAOnClass2Meth1 = [type(a) for a in AttributeA.GetAttributes(Class2.meth1)] foundAttributesAOnClass2Meth2 = [type(a) for a in AttributeA.GetAttributes(Class2.meth2)] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(4, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth2, Class2.meth1, Class2.meth2]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) self.assertListEqual(foundAttributesAOnClass1Meth2, [AttributeA]) self.assertEqual(0, len(foundAttributesAOnClass2Meth0)) self.assertEqual(1, len(foundAttributesAOnClass2Meth1)) self.assertListEqual(foundAttributesAOnClass2Meth1, [AttributeA]) self.assertEqual(1, len(foundAttributesAOnClass2Meth2)) self.assertListEqual(foundAttributesAOnClass2Meth2, [AttributeA]) def test_MultipleAttributes_SingleClass_SingleMethod(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass @AttributeB() def meth2(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundFunctionsOnAttributeB = [f for f in AttributeB.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundMethodsOnAttributeB = [m for m in AttributeB.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [type(a) for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesBOnClass1Meth0 = [type(b) for b in AttributeB.GetAttributes(Class1.meth0)] foundAttributesBOnClass1Meth1 = [type(b) for b in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [type(b) for b in AttributeB.GetAttributes(Class1.meth2)] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(0, len(foundFunctionsOnAttributeB)) self.assertEqual(1, len(foundMethodsOnAttributeA)) self.assertEqual(2, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1]) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth1, Class1.meth2]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) self.assertEqual(0, len(foundAttributesAOnClass1Meth2)) self.assertEqual(0, len(foundAttributesBOnClass1Meth0)) self.assertEqual(1, len(foundAttributesBOnClass1Meth1)) self.assertListEqual(foundAttributesBOnClass1Meth1, [AttributeB]) self.assertEqual(1, len(foundAttributesBOnClass1Meth2)) self.assertListEqual(foundAttributesBOnClass1Meth2, [AttributeB]) def test_MultipleAttributes_SingleClass_MultipleMethods(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass @AttributeA() def meth2(self): pass @AttributeB() def meth3(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundFunctionsOnAttributeB = [f for f in AttributeB.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundMethodsOnAttributeB = [m for m in AttributeB.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [type(a) for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass1Meth3 = [type(a) for a in AttributeA.GetAttributes(Class1.meth3)] foundAttributesBOnClass1Meth0 = [type(b) for b in AttributeB.GetAttributes(Class1.meth0)] foundAttributesBOnClass1Meth1 = [type(b) for b in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [type(b) for b in AttributeB.GetAttributes(Class1.meth2)] foundAttributesBOnClass1Meth3 = [type(b) for b in AttributeB.GetAttributes(Class1.meth3)] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(0, len(foundFunctionsOnAttributeB)) self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertEqual(2, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth2]) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth1, Class1.meth3]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) self.assertListEqual(foundAttributesAOnClass1Meth2, [AttributeA]) self.assertEqual(0, len(foundAttributesAOnClass1Meth3)) self.assertEqual(0, len(foundAttributesBOnClass1Meth0)) self.assertEqual(1, len(foundAttributesBOnClass1Meth1)) self.assertListEqual(foundAttributesBOnClass1Meth1, [AttributeB]) self.assertEqual(0, len(foundAttributesBOnClass1Meth2)) self.assertEqual(1, len(foundAttributesBOnClass1Meth3)) self.assertListEqual(foundAttributesBOnClass1Meth3, [AttributeB]) def test_MultipleAttributes_MultipleClasses_SingleMethod(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass @AttributeB() def meth2(self): pass class Class2(metaclass=ExtendedType): def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundFunctionsOnAttributeB = [f for f in AttributeB.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundMethodsOnAttributeB = [m for m in AttributeB.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [type(a) for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass2Meth0 = [type(a) for a in AttributeA.GetAttributes(Class2.meth0)] foundAttributesAOnClass2Meth1 = [type(a) for a in AttributeA.GetAttributes(Class2.meth1)] foundAttributesBOnClass1Meth0 = [type(b) for b in AttributeB.GetAttributes(Class1.meth0)] foundAttributesBOnClass1Meth1 = [type(b) for b in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [type(b) for b in AttributeB.GetAttributes(Class1.meth2)] foundAttributesBOnClass2Meth0 = [type(b) for b in AttributeB.GetAttributes(Class2.meth0)] foundAttributesBOnClass2Meth1 = [type(b) for b in AttributeB.GetAttributes(Class2.meth1)] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(0, len(foundFunctionsOnAttributeB)) self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertEqual(3, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class2.meth1]) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth1, Class1.meth2, Class2.meth1]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) self.assertEqual(0, len(foundAttributesAOnClass1Meth2)) self.assertEqual(0, len(foundAttributesAOnClass2Meth0)) self.assertEqual(1, len(foundAttributesAOnClass2Meth1)) self.assertListEqual(foundAttributesAOnClass2Meth1, [AttributeA]) self.assertEqual(0, len(foundAttributesBOnClass1Meth0)) self.assertEqual(1, len(foundAttributesBOnClass1Meth1)) self.assertListEqual(foundAttributesBOnClass1Meth1, [AttributeB]) self.assertEqual(1, len(foundAttributesBOnClass1Meth2)) self.assertListEqual(foundAttributesBOnClass1Meth2, [AttributeB]) self.assertEqual(0, len(foundAttributesBOnClass2Meth0)) self.assertEqual(1, len(foundAttributesBOnClass2Meth1)) self.assertListEqual(foundAttributesBOnClass2Meth1, [AttributeB]) def test_MultipleAttributes_MultipleClasses_MultipleMethods(self) -> None: class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() @AttributeB() def meth1(self): pass @AttributeA() def meth2(self): pass class Class2(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeA() @AttributeB() def meth2(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundFunctionsOnAttributeB = [f for f in AttributeB.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundMethodsOnAttributeB = [m for m in AttributeB.GetMethods()] foundAttributesAOnClass1Meth0 = [type(a) for a in AttributeA.GetAttributes(Class1.meth0)] foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [type(a) for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass2Meth0 = [type(a) for a in AttributeA.GetAttributes(Class2.meth0)] foundAttributesAOnClass2Meth1 = [type(a) for a in AttributeA.GetAttributes(Class2.meth1)] foundAttributesAOnClass2Meth2 = [type(a) for a in AttributeA.GetAttributes(Class2.meth2)] foundAttributesBOnClass1Meth0 = [type(b) for b in AttributeB.GetAttributes(Class1.meth0)] foundAttributesBOnClass1Meth1 = [type(b) for b in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [type(b) for b in AttributeB.GetAttributes(Class1.meth2)] foundAttributesBOnClass2Meth0 = [type(b) for b in AttributeB.GetAttributes(Class2.meth0)] foundAttributesBOnClass2Meth1 = [type(b) for b in AttributeB.GetAttributes(Class2.meth1)] foundAttributesBOnClass2Meth2 = [type(b) for b in AttributeB.GetAttributes(Class2.meth2)] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(0, len(foundFunctionsOnAttributeB)) self.assertEqual(4, len(foundMethodsOnAttributeA)) self.assertEqual(2, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth2, Class2.meth1, Class2.meth2]) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth1, Class2.meth2]) self.assertEqual(0, len(foundAttributesAOnClass1Meth0)) self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) self.assertListEqual(foundAttributesAOnClass1Meth2, [AttributeA]) self.assertEqual(0, len(foundAttributesAOnClass2Meth0)) self.assertEqual(1, len(foundAttributesAOnClass2Meth1)) self.assertListEqual(foundAttributesAOnClass2Meth1, [AttributeA]) self.assertEqual(1, len(foundAttributesAOnClass2Meth2)) self.assertListEqual(foundAttributesAOnClass2Meth2, [AttributeA]) self.assertEqual(0, len(foundAttributesBOnClass1Meth0)) self.assertEqual(1, len(foundAttributesBOnClass1Meth1)) self.assertListEqual(foundAttributesBOnClass1Meth1, [AttributeB]) self.assertEqual(0, len(foundAttributesBOnClass1Meth2)) self.assertEqual(0, len(foundAttributesBOnClass2Meth0)) self.assertEqual(0, len(foundAttributesBOnClass2Meth1)) self.assertEqual(1, len(foundAttributesBOnClass2Meth2)) self.assertListEqual(foundAttributesBOnClass2Meth2, [AttributeB]) class MetaTesting(TestCase): def test_Meta(self) -> None: print() class AttributeA(Attribute): pass class AttributeB(Attribute): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeB() def meth2(self): pass @AttributeA() @AttributeB() def meth3(self): pass @AttributeA() @AttributeB() @AttributeB() def meth4(self): pass foundFunctionsOnAttributeA = [f for f in AttributeA.GetFunctions()] foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundMethodsOnAttributeB = [m for m in AttributeB.GetMethods()] self.assertEqual(0, len(foundFunctionsOnAttributeA)) self.assertEqual(3, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class1.meth3, Class1.meth4]) self.assertEqual(4, len(foundMethodsOnAttributeB)) self.assertListEqual(foundMethodsOnAttributeB, [Class1.meth2, Class1.meth3, Class1.meth4, Class1.meth4]) class GetAttributesFiltering(TestCase): pass # default filter # no filter # subclasses # tuple filter (or) class Filtering(TestCase): def test_Scope_Class(self) -> None: class AttributeA(Attribute): pass class AttributeAA(AttributeA): pass class Class1(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeAA() def meth2(self): pass class Class2(metaclass=ExtendedType): def meth0(self): pass @AttributeA() def meth1(self): pass @AttributeAA() def meth2(self): pass foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] foundMethodsOnAttributeAA = [m for m in AttributeAA.GetMethods()] self.assertEqual(2, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Class1.meth1, Class2.meth1]) self.assertEqual(2, len(foundMethodsOnAttributeAA)) self.assertListEqual(foundMethodsOnAttributeAA, [Class1.meth2, Class2.meth2]) foundMethodsOnAttributeAScopedToClass1 = [m for m in AttributeA.GetMethods(scope=Class1)] foundMethodsOnAttributeAScopedToClass2 = [m for m in AttributeA.GetMethods(scope=Class2)] self.assertListEqual(foundMethodsOnAttributeAScopedToClass1, [Class1.meth1]) self.assertListEqual(foundMethodsOnAttributeAScopedToClass2, [Class2.meth1]) class Attribute_GetAttributes_Filtering(TestCase): def test_1(self) -> None: class AttributeA(Attribute): pass class AttributeAA(AttributeA): pass class AttributeB(Attribute): pass class Class1(metaclass=ExtendedType): @AttributeA() def meth1(self): pass @AttributeAA() def meth2(self): pass @AttributeB() def meth3(self): pass foundAttributesAOnClass1Meth1 = [type(a) for a in AttributeA.GetAttributes(Class1.meth1)] foundAttributesAOnClass1Meth2 = [type(a) for a in AttributeA.GetAttributes(Class1.meth2)] foundAttributesAOnClass1Meth3 = [type(a) for a in AttributeA.GetAttributes(Class1.meth3)] foundAttributesAAOnClass1Meth1 = [type(a) for a in AttributeAA.GetAttributes(Class1.meth1)] foundAttributesAAOnClass1Meth2 = [type(a) for a in AttributeAA.GetAttributes(Class1.meth2)] foundAttributesAAOnClass1Meth3 = [type(a) for a in AttributeAA.GetAttributes(Class1.meth3)] foundAttributesBOnClass1Meth1 = [type(a) for a in AttributeB.GetAttributes(Class1.meth1)] foundAttributesBOnClass1Meth2 = [type(a) for a in AttributeB.GetAttributes(Class1.meth2)] foundAttributesBOnClass1Meth3 = [type(a) for a in AttributeB.GetAttributes(Class1.meth3)] self.assertEqual(1, len(foundAttributesAOnClass1Meth1)) self.assertListEqual(foundAttributesAOnClass1Meth1, [AttributeA]) self.assertEqual(1, len(foundAttributesAOnClass1Meth2)) self.assertListEqual(foundAttributesAOnClass1Meth2, [AttributeAA]) self.assertEqual(0, len(foundAttributesAOnClass1Meth3)) self.assertEqual(0, len(foundAttributesAAOnClass1Meth1)) self.assertEqual(1, len(foundAttributesAAOnClass1Meth2)) self.assertListEqual(foundAttributesAAOnClass1Meth2, [AttributeAA]) self.assertEqual(0, len(foundAttributesAAOnClass1Meth3)) self.assertEqual(0, len(foundAttributesBOnClass1Meth1)) self.assertEqual(0, len(foundAttributesBOnClass1Meth2)) self.assertEqual(1, len(foundAttributesBOnClass1Meth3)) self.assertListEqual(foundAttributesBOnClass1Meth3, [AttributeB]) class MultipleInheritance(TestCase): def test_1(self) -> None: class AttributeA(Attribute): pass class Part_A(metaclass=ExtendedType, mixin=True): @AttributeA() def meth1(self): pass class Part_B(metaclass=ExtendedType, mixin=True): @AttributeA() def meth2(self): pass class Common(Part_A, Part_B, metaclass=ExtendedType, slots=True): @AttributeA() def meth0(self): pass foundMethodsOnAttributeA = [m for m in AttributeA.GetMethods()] self.assertEqual(3, len(foundMethodsOnAttributeA)) self.assertListEqual(foundMethodsOnAttributeA, [Part_A.meth1, Part_B.meth2, Common.meth0]) common = Common() foundMethodsUsingAttributeA = common.GetMethodsWithAttributes(AttributeA) self.assertEqual(3, len(foundMethodsUsingAttributeA)) self.assertListEqual(list(foundMethodsUsingAttributeA), [Part_A.meth1, Part_B.meth2, Common.meth0]) # default filter # no filter # subclasses # tuple filter (or) pyTooling-8.11.0/tests/unit/Attributes/PredefinedAttributes.py000066400000000000000000000145731513317154500245410ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for attributes attached to methods. """ from unittest import TestCase from pyTooling.Common import firstItem from pyTooling.MetaClasses import ExtendedType from pyTooling.Attributes import SimpleAttribute, Attribute, Entity if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Simple(TestCase): def test_ClassAttribute(self) -> None: @SimpleAttribute(1, 2, id="my", name="Class1") class MyClass1(metaclass=ExtendedType): pass @SimpleAttribute(3, 4, id="my", name="Class2") class MyClass2(metaclass=ExtendedType): pass expectedList = [ ((1, 2), {"id": "my", "name": "Class1"}), ((3, 4), {"id": "my", "name": "Class2"}), ] for cls, expected in zip(SimpleAttribute.GetClasses(), expectedList): attr = firstItem(cls.__pyattr__) self.assertTupleEqual(expected[0], attr.Args) self.assertDictEqual(expected[1], attr.KwArgs) def test_MethodAttribute(self) -> None: class MyClass1(metaclass=ExtendedType): @SimpleAttribute(1, 2, id="my", name="Class1") def method(self): pass class MyClass2(metaclass=ExtendedType): @SimpleAttribute(3, 4, id="my", name="Class2") def method(self): pass expectedList = [ ((1, 2), {"id": "my", "name": "Class1"}), ((3, 4), {"id": "my", "name": "Class2"}), ] for cls, expected in zip(SimpleAttribute.GetMethods(), expectedList): attr = firstItem(cls.__pyattr__) self.assertTupleEqual(expected[0], attr.Args) self.assertDictEqual(expected[1], attr.KwArgs) def test_FunctionAttribute(self) -> None: @SimpleAttribute(1, 2, id="my", name="Func1") def function1(): pass @SimpleAttribute(3, 4, id="my", name="Func2") def function2(): pass expectedList = [ ((1, 2), {"id": "my", "name": "Func1"}), ((3, 4), {"id": "my", "name": "Func2"}), ] for cls, expected in zip(SimpleAttribute.GetFunctions(), expectedList): attr = firstItem(cls.__pyattr__) self.assertTupleEqual(expected[0], attr.Args) self.assertDictEqual(expected[1], attr.KwArgs) class MySimpleAttribute(SimpleAttribute): pass class GroupAttribute(Attribute): _id: str def __init__(self, identifier: str) -> None: self._id = identifier def __call__(self, entity: Entity) -> Entity: self._AppendAttribute(entity, MySimpleAttribute(3, 4, id=self._id, name="attr1")) self._AppendAttribute(entity, MySimpleAttribute(5, 6, id=self._id, name="attr2")) return entity class Grouped(TestCase): def test_Group_Simple(self) -> None: @MySimpleAttribute(1, 2, id="my", name="Class1") @GroupAttribute("grp") class MyClass1: pass foundClasses = [c for c in MySimpleAttribute.GetClasses()] self.assertEqual(3, len(foundClasses)) for c in foundClasses: self.assertIs(MyClass1, c) pyTooling-8.11.0/tests/unit/Attributes/__init__.py000066400000000000000000000111271513317154500221540ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Helper functions for unittests. """ from contextlib import contextmanager from io import StringIO import sys as _sys from typing import Generator, Dict, TypeVar, Tuple K1 = TypeVar("K1") V1 = TypeVar("V1") K2 = TypeVar("K2") V2 = TypeVar("V2") def zip(dict1: Dict[K1, V1], dict2: Dict[K2, V2]) -> Generator[Tuple[K1, K2, V1, V2], None, None]: l1 = len(dict1) l2 = len(dict2) if l1 != l2: if l1 < l2: raise ValueError(f"'dict1' (len={l1}) has less elements than 'dict2' (len={l2}).") else: raise ValueError(f"'dict1' (len={l1}) has more elements than 'dict2' (len={l2}).") iter1 = iter(dict1.items()) iter2 = iter(dict2.items()) try: while True: key1, value1 = next(iter1) key2, value2 = next(iter2) yield key1, key2, value1, value2 except StopIteration: return @contextmanager def CapturePrintContext(): old_out = _sys.stdout old_err = _sys.stderr try: _sys.stdout = StringIO() _sys.stderr = StringIO() yield _sys.stdout, _sys.stderr finally: _sys.stdout = old_out _sys.stderr = old_err pyTooling-8.11.0/tests/unit/CLIAbstraction/000077500000000000000000000000001513317154500205145ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/CLIAbstraction/Argument.py000066400000000000000000001114311513317154500226510ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Testcases for arguments without a prefix.""" from pathlib import Path from unittest import TestCase from pyTooling.CLIAbstraction.Argument import StringArgument, DelimiterArgument, CommandLineArgument, NamedArgument, \ ValuedArgument, NamedAndValuedArgument, PathArgument, StringListArgument, PathListArgument, ExecutableArgument, \ NamedTupledArgument from pyTooling.CLIAbstraction.BooleanFlag import BooleanFlag, ShortBooleanFlag, LongBooleanFlag, WindowsBooleanFlag from pyTooling.CLIAbstraction.Command import CommandArgument, ShortCommand, WindowsCommand, LongCommand from pyTooling.CLIAbstraction.Flag import FlagArgument, ShortFlag, WindowsFlag, LongFlag from pyTooling.CLIAbstraction.KeyValueFlag import NamedKeyValuePairsArgument, ShortKeyValueFlag, LongKeyValueFlag, \ WindowsKeyValueFlag from pyTooling.CLIAbstraction.OptionalValuedFlag import OptionalValuedFlag, ShortOptionalValuedFlag, \ WindowsOptionalValuedFlag, LongOptionalValuedFlag from pyTooling.CLIAbstraction.ValuedFlag import ShortValuedFlag, WindowsValuedFlag, LongValuedFlag, ValuedFlag from pyTooling.CLIAbstraction.ValuedFlagList import ShortValuedFlagList, ValuedFlagList, WindowsValuedFlagList, \ LongValuedFlagList from pyTooling.CLIAbstraction.ValuedTupleFlag import ShortTupleFlag, WindowsTupleFlag, LongTupleFlag if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class WithoutPrefix(TestCase): def test_CommandLineArgument(self) -> None: with self.assertRaises(TypeError): _ = CommandLineArgument() def test_ExecutableArgument(self) -> None: with self.assertRaises(TypeError): _ = ExecutableArgument("program.exe") executablePath = Path("program.exe") argument = ExecutableArgument(executablePath) self.assertIs(executablePath, argument.Executable) self.assertEqual(f"{executablePath}", argument.AsArgument()) self.assertEqual(f"\"{executablePath}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(TypeError): argument.Executable = "script.sh" executablePath2 = Path("script.sh") argument.Executable = executablePath2 self.assertIs(executablePath2, argument.Executable) def test_DelimiterArgument(self) -> None: pattern = "--" argument = DelimiterArgument() self.assertEqual(f"{pattern}", argument.AsArgument()) self.assertEqual(f"\"{pattern}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) def test_DerivedDelimiterArgument(self) -> None: pattern = "++" class Delimiter(DelimiterArgument, pattern=pattern): pass argument = Delimiter() self.assertEqual(f"{pattern}", argument.AsArgument()) self.assertEqual(f"\"{pattern}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) def test_AbstractCommandArgument(self) -> None: with self.assertRaises(TypeError): _ = CommandArgument() def test_CommandArgument(self) -> None: name = "command" class Command(CommandArgument, name=name): pass argument = Command() self.assertIs(name, argument.Name) self.assertEqual(f"{name}", argument.AsArgument()) self.assertEqual(f"\"{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_NamedArgument(self) -> None: with self.assertRaises(TypeError): _ = NamedArgument() def test_DerivedNamedArgument(self) -> None: name = "command" class Command(CommandArgument, name=name): pass argument = Command() self.assertIs(name, argument.Name) self.assertEqual(f"{name}", argument.AsArgument()) self.assertEqual(f"\"{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "command2" def test_ValuedArgument(self) -> None: value = "value" argument = ValuedArgument(value) self.assertIs(value, argument.Value) self.assertEqual(f"{value}", argument.AsArgument()) self.assertEqual(f"\"{value}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) def test_NamedAndValuedArgument(self) -> None: with self.assertRaises(TypeError): _ = NamedAndValuedArgument() def test_DerivedNamedAndValuedArgument(self) -> None: name = "flag" value = "value" class Flag(NamedAndValuedArgument, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertEqual(f"{name}={value}", argument.AsArgument()) self.assertEqual(f"\"{name}={value}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_StringArgument(self) -> None: value = "value" argument = StringArgument(value) self.assertIs(value, argument.Value) self.assertEqual(f"{value}", argument.AsArgument()) self.assertEqual(f"\"{value}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "value2" argument.Value = value2 self.assertIs(value2, argument.Value) def test_StringListArgument(self) -> None: values = ("value1", "value2") argument = StringListArgument(values) self.assertListEqual(list(values), argument.Value) self.assertEqual([f"{value}" for value in values], argument.AsArgument()) self.assertEqual(f"\"{values[0]}\" \"{values[1]}\"", str(argument)) self.assertEqual(f"\"{values[0]}\", \"{values[1]}\"", repr(argument)) with self.assertRaises(TypeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = (42, "bar") values2 = ("631", "527") argument.Value = values2 self.assertListEqual(list(values2), argument.Value) self.assertListEqual([f"{value}" for value in values2], argument.AsArgument()) self.assertEqual(f"\"{values2[0]}\" \"{values2[1]}\"", str(argument)) self.assertEqual(f"\"{values2[0]}\", \"{values2[1]}\"", repr(argument)) def test_PathArgument(self) -> None: path = Path("file1.txt") argument = PathArgument(path) self.assertIs(path, argument.Value) self.assertEqual(f"{path}", argument.AsArgument()) self.assertEqual(f"\"{path}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) path2 = Path("file2.txt") argument.Value = path2 self.assertIs(path2, argument.Value) def test_PathListArgument(self) -> None: values = (Path("file1.txt"), Path("file2.txt")) argument = PathListArgument(values) self.assertListEqual(list(values), argument.Value) self.assertEqual([f"{value}" for value in values], argument.AsArgument()) self.assertEqual(f"\"{values[0]}\" \"{values[1]}\"", str(argument)) self.assertEqual(f"\"{values[0]}\", \"{values[1]}\"", repr(argument)) with self.assertRaises(TypeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = (42, Path("file3.txt")) values2 = (Path("file1.log"), Path("file2.log")) argument.Value = values2 self.assertListEqual(list(values2), argument.Value) self.assertListEqual([f"{value}" for value in values2], argument.AsArgument()) self.assertEqual(f"\"{values2[0]}\" \"{values2[1]}\"", str(argument)) self.assertEqual(f"\"{values2[0]}\", \"{values2[1]}\"", repr(argument)) class Commands(TestCase): def test_ShortCommand(self) -> None: with self.assertRaises(TypeError): _ = ShortCommand() def test_DerivedShortCommand(self) -> None: name = "command" class Command(ShortCommand, name=name): pass argument = Command() self.assertIs(name, argument.Name) self.assertEqual(f"-{name}", argument.AsArgument()) self.assertEqual(f"\"-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "command2" def test_LongCommand(self) -> None: with self.assertRaises(TypeError): _ = LongCommand() def test_DerivedLongCommand(self) -> None: name = "command" class Command(LongCommand, name=name): pass argument = Command() self.assertIs(name, argument.Name) self.assertEqual(f"--{name}", argument.AsArgument()) self.assertEqual(f"\"--{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "command2" def test_WindowsCommand(self) -> None: with self.assertRaises(TypeError): _ = WindowsCommand() def test_DerivedWindowsCommand(self) -> None: name = "command" class Command(WindowsCommand, name=name): pass argument = Command() self.assertIs(name, argument.Name) self.assertEqual(f"/{name}", argument.AsArgument()) self.assertEqual(f"\"/{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "command2" class Flags(TestCase): def test_FlagArgument(self) -> None: with self.assertRaises(TypeError): _ = FlagArgument() def test_DerivedFlagArgument(self) -> None: name = "flag" class Flag(FlagArgument, name=name): pass argument = Flag() self.assertIs(name, argument.Name) self.assertEqual(f"{name}", argument.AsArgument()) self.assertEqual(f"\"{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_ShortFlagArgument(self) -> None: with self.assertRaises(TypeError): _ = ShortFlag() def test_DerivedShortFlagArgument(self) -> None: name = "flag" class Flag(ShortFlag, name=name): pass argument = Flag() self.assertIs(name, argument.Name) self.assertEqual(f"-{name}", argument.AsArgument()) self.assertEqual(f"\"-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_LongFlagArgument(self) -> None: with self.assertRaises(TypeError): _ = LongFlag() def test_DerivedLongFlagArgument(self) -> None: name = "flag" class Flag(LongFlag, name=name): pass argument = Flag() self.assertIs(name, argument.Name) self.assertEqual(f"--{name}", argument.AsArgument()) self.assertEqual(f"\"--{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_WindowsFlagArgument(self) -> None: with self.assertRaises(TypeError): _ = WindowsFlag() def test_DerivedWindowsFlagArgument(self) -> None: name = "flag" class Flag(WindowsFlag, name=name): pass argument = Flag() self.assertIs(name, argument.Name) self.assertEqual(f"/{name}", argument.AsArgument()) self.assertEqual(f"\"/{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" class BooleanFlags(TestCase): def test_BooleanFlagArgument(self) -> None: with self.assertRaises(TypeError): _ = BooleanFlag() def test_DerivedBooleanFlagArgument(self) -> None: name = "flag" class Flag(BooleanFlag, name=name): pass argument = Flag(True) self.assertIs(name, argument.Name) self.assertTrue(argument.Value) self.assertEqual(f"with-{name}", argument.AsArgument()) self.assertEqual(f"\"with-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) argument.Value = False self.assertFalse(argument.Value) self.assertEqual(f"without-{name}", argument.AsArgument()) self.assertEqual(f"\"without-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_ShortBooleanFlagArgument(self) -> None: with self.assertRaises(TypeError): _ = ShortBooleanFlag() def test_DerivedShortBooleanFlagArgument(self) -> None: name = "flag" class Flag(ShortBooleanFlag, name=name): pass argument = Flag(True) self.assertIs(name, argument.Name) self.assertTrue(argument.Value) self.assertEqual(f"-with-{name}", argument.AsArgument()) self.assertEqual(f"\"-with-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) argument.Value = False self.assertFalse(argument.Value) self.assertEqual(f"-without-{name}", argument.AsArgument()) self.assertEqual(f"\"-without-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_LongBooleanFlagArgument(self) -> None: with self.assertRaises(TypeError): _ = LongBooleanFlag() def test_DerivedLongBooleanFlagArgument(self) -> None: name = "flag" class Flag(LongBooleanFlag, name=name): pass argument = Flag(True) self.assertIs(name, argument.Name) self.assertTrue(argument.Value) self.assertEqual(f"--with-{name}", argument.AsArgument()) self.assertEqual(f"\"--with-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) argument.Value = False self.assertFalse(argument.Value) self.assertEqual(f"--without-{name}", argument.AsArgument()) self.assertEqual(f"\"--without-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_WindowsBooleanFlagArgument(self) -> None: with self.assertRaises(TypeError): _ = WindowsBooleanFlag() def test_DerivedWindowsBooleanFlagArgument(self) -> None: name = "flag" class Flag(WindowsBooleanFlag, name=name): pass argument = Flag(True) self.assertIs(name, argument.Name) self.assertTrue(argument.Value) self.assertEqual(f"/with-{name}", argument.AsArgument()) self.assertEqual(f"\"/with-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) argument.Value = False self.assertFalse(argument.Value) self.assertEqual(f"/without-{name}", argument.AsArgument()) self.assertEqual(f"\"/without-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" class OptionalValuedFlags(TestCase): def test_OptionalValuedFlag(self) -> None: with self.assertRaises(TypeError): _ = OptionalValuedFlag() def test_DerivedOptionalValuedFlag(self) -> None: name = "flag" value = None class Flag(OptionalValuedFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertIsNone(argument.Value) self.assertEqual(f"{name}", argument.AsArgument()) self.assertEqual(f"\"{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "42" argument.Value = value2 self.assertIs(value2, argument.Value) self.assertEqual(f"{name}={value2}", argument.AsArgument()) self.assertEqual(f"\"{name}={value2}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_ShortOptionalValuedFlag(self) -> None: with self.assertRaises(TypeError): _ = ShortOptionalValuedFlag() def test_DerivedShortOptionalValuedFlag(self) -> None: name = "flag" value = None class Flag(ShortOptionalValuedFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertIsNone(argument.Value) self.assertEqual(f"-{name}", argument.AsArgument()) self.assertEqual(f"\"-{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "42" argument.Value = value2 self.assertIs(value2, argument.Value) self.assertEqual(f"-{name}={value2}", argument.AsArgument()) self.assertEqual(f"\"-{name}={value2}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_LongOptionalValuedFlag(self) -> None: with self.assertRaises(TypeError): _ = LongOptionalValuedFlag() def test_DerivedLongOptionalValuedFlag(self) -> None: name = "flag" value = None class Flag(LongOptionalValuedFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertIsNone(argument.Value) self.assertEqual(f"--{name}", argument.AsArgument()) self.assertEqual(f"\"--{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "42" argument.Value = value2 self.assertIs(value2, argument.Value) self.assertEqual(f"--{name}={value2}", argument.AsArgument()) self.assertEqual(f"\"--{name}={value2}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_WindowsOptionalValuedFlag(self) -> None: with self.assertRaises(TypeError): _ = WindowsOptionalValuedFlag() def test_DerivedWindowsOptionalValuedFlag(self) -> None: name = "flag" value = None class Flag(WindowsOptionalValuedFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertIsNone(argument.Value) self.assertEqual(f"/{name}", argument.AsArgument()) self.assertEqual(f"\"/{name}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "42" argument.Value = value2 self.assertIs(value2, argument.Value) self.assertEqual(f"/{name}:{value2}", argument.AsArgument()) self.assertEqual(f"\"/{name}:{value2}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" class ValuedFlags(TestCase): def test_ValuedFlag(self) -> None: with self.assertRaises(TypeError): _ = ValuedFlag() def test_DerivedValuedFlag(self) -> None: name = "flag" value = "42" class Flag(ValuedFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertIs(value, argument.Value) self.assertEqual(f"{name}={value}", argument.AsArgument()) self.assertEqual(f"\"{name}={value}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "84" argument.Value = value2 self.assertIs(value2, argument.Value) self.assertEqual(f"{name}={value2}", argument.AsArgument()) self.assertEqual(f"\"{name}={value2}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_ShortValuedFlag(self) -> None: with self.assertRaises(TypeError): _ = ShortValuedFlag() def test_DerivedShortValuedFlag(self) -> None: name = "flag" value = "42" class Flag(ShortValuedFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertIs(value, argument.Value) self.assertEqual(f"-{name}={value}", argument.AsArgument()) self.assertEqual(f"\"-{name}={value}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "84" argument.Value = value2 self.assertIs(value2, argument.Value) self.assertEqual(f"-{name}={value2}", argument.AsArgument()) self.assertEqual(f"\"-{name}={value2}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_LongValuedFlag(self) -> None: with self.assertRaises(TypeError): _ = LongValuedFlag() def test_DerivedLongValuedFlag(self) -> None: name = "flag" value = "42" class Flag(LongValuedFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertIs(value, argument.Value) self.assertEqual(f"--{name}={value}", argument.AsArgument()) self.assertEqual(f"\"--{name}={value}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "84" argument.Value = value2 self.assertIs(value2, argument.Value) self.assertEqual(f"--{name}={value2}", argument.AsArgument()) self.assertEqual(f"\"--{name}={value2}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_WindowsValuedFlag(self) -> None: with self.assertRaises(TypeError): _ = WindowsValuedFlag() def test_DerivedWindowsValuedFlag(self) -> None: name = "flag" value = "42" class Flag(WindowsValuedFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertIs(value, argument.Value) self.assertEqual(f"/{name}:{value}", argument.AsArgument()) self.assertEqual(f"\"/{name}:{value}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) value2 = "84" argument.Value = value2 self.assertIs(value2, argument.Value) self.assertEqual(f"/{name}:{value2}", argument.AsArgument()) self.assertEqual(f"\"/{name}:{value2}\"", str(argument)) self.assertEqual(str(argument), repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" class ValuedFlagLists(TestCase): def test_ValuedFlagList(self) -> None: with self.assertRaises(TypeError): _ = ValuedFlagList() def test_DerivedValuedFlagList(self) -> None: name = "flag" values = ("42", "84") class Flag(ValuedFlagList, name=name): pass argument = Flag(values) self.assertIs(name, argument.Name) self.assertListEqual(list(values), argument.Value) self.assertListEqual([f"{name}={val}" for val in values], argument.AsArgument()) self.assertEqual(f"\"{name}={values[0]}\" \"{name}={values[1]}\"", str(argument)) self.assertEqual(f"\"{name}={values[0]}\", \"{name}={values[1]}\"", repr(argument)) with self.assertRaises(TypeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = (42, "bar") values2 = ("631", "527") argument.Value = values2 self.assertListEqual(list(values2), argument.Value) self.assertListEqual([f"{name}={val}" for val in values2], argument.AsArgument()) self.assertEqual(f"\"{name}={values2[0]}\" \"{name}={values2[1]}\"", str(argument)) self.assertEqual(f"\"{name}={values2[0]}\", \"{name}={values2[1]}\"", repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_ShortValuedFlagList(self) -> None: with self.assertRaises(TypeError): _ = ShortValuedFlagList() def test_DerivedShortValuedFlagList(self) -> None: name = "flag" values = ("42", "84") class Flag(ShortValuedFlagList, name=name): pass argument = Flag(values) self.assertIs(name, argument.Name) self.assertListEqual(list(values), argument.Value) self.assertListEqual([f"-{name}={val}" for val in values], argument.AsArgument()) self.assertEqual(f"\"-{name}={values[0]}\" \"-{name}={values[1]}\"", str(argument)) self.assertEqual(f"\"-{name}={values[0]}\", \"-{name}={values[1]}\"", repr(argument)) with self.assertRaises(TypeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = (42, "bar") values2 = ("631", "527") argument.Value = values2 self.assertListEqual(list(values2), argument.Value) self.assertListEqual([f"-{name}={val}" for val in values2], argument.AsArgument()) self.assertEqual(f"\"-{name}={values2[0]}\" \"-{name}={values2[1]}\"", str(argument)) self.assertEqual(f"\"-{name}={values2[0]}\", \"-{name}={values2[1]}\"", repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_LongValuedFlagList(self) -> None: with self.assertRaises(TypeError): _ = LongValuedFlagList() def test_DerivedLongValuedFlagList(self) -> None: name = "flag" values = ("42", "84") class Flag(LongValuedFlagList, name=name): pass argument = Flag(values) self.assertIs(name, argument.Name) self.assertListEqual(list(values), argument.Value) self.assertListEqual([f"--{name}={val}" for val in values], argument.AsArgument()) self.assertEqual(f"\"--{name}={values[0]}\" \"--{name}={values[1]}\"", str(argument)) self.assertEqual(f"\"--{name}={values[0]}\", \"--{name}={values[1]}\"", repr(argument)) with self.assertRaises(TypeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = (42, "bar") values2 = ("631", "527") argument.Value = values2 self.assertListEqual(list(values2), argument.Value) self.assertListEqual([f"--{name}={val}" for val in values2], argument.AsArgument()) self.assertEqual(f"\"--{name}={values2[0]}\" \"--{name}={values2[1]}\"", str(argument)) self.assertEqual(f"\"--{name}={values2[0]}\", \"--{name}={values2[1]}\"", repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_WindowsValuedFlagList(self) -> None: with self.assertRaises(TypeError): _ = WindowsValuedFlagList() def test_DerivedWindowsValuedFlagList(self) -> None: name = "flag" values = ("42", "84") class Flag(WindowsValuedFlagList, name=name): pass argument = Flag(values) self.assertIs(name, argument.Name) self.assertListEqual(list(values), argument.Value) self.assertListEqual([f"/{name}:{val}" for val in values], argument.AsArgument()) self.assertEqual(f"\"/{name}:{values[0]}\" \"/{name}:{values[1]}\"", str(argument)) self.assertEqual(f"\"/{name}:{values[0]}\", \"/{name}:{values[1]}\"", repr(argument)) with self.assertRaises(TypeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = (42, "bar") values2 = ("631", "527") argument.Value = values2 self.assertListEqual(list(values2), argument.Value) self.assertListEqual([f"/{name}:{val}" for val in values2], argument.AsArgument()) self.assertEqual(f"\"/{name}:{values2[0]}\" \"/{name}:{values2[1]}\"", str(argument)) self.assertEqual(f"\"/{name}:{values2[0]}\", \"/{name}:{values2[1]}\"", repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" class ValuedTupleFlags(TestCase): def test_ValuedTupleArgument(self) -> None: with self.assertRaises(TypeError): _ = NamedTupledArgument() def test_DerivedValuedTupleArgument(self) -> None: name = "flag" value = "42" class Flag(NamedTupledArgument, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertEqual(value, argument.Value) self.assertListEqual([f"{name}", f"{value}"], list(argument.AsArgument())) self.assertEqual(f"\"{name}\" \"{value}\"", str(argument)) self.assertEqual(f"\"{name}\", \"{value}\"", repr(argument)) # with self.assertRaises(TypeError): # argument.Value = 42 value2 = "84" argument.Value = value2 self.assertEqual(value2, argument.Value) self.assertListEqual([f"{name}", f"{value2}"], list(argument.AsArgument())) self.assertEqual(f"\"{name}\" \"{value2}\"", str(argument)) self.assertEqual(f"\"{name}\", \"{value2}\"", repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_ShortTupleFlag(self) -> None: with self.assertRaises(TypeError): _ = ShortTupleFlag() def test_DerivedShortTupleFlag(self) -> None: name = "flag" value = "42" class Flag(ShortTupleFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertEqual(value, argument.Value) self.assertListEqual([f"-{name}", f"{value}"], list(argument.AsArgument())) self.assertEqual(f"\"-{name}\" \"{value}\"", str(argument)) self.assertEqual(f"\"-{name}\", \"{value}\"", repr(argument)) # with self.assertRaises(TypeError): # argument.Value = 42 value2 = "84" argument.Value = value2 self.assertEqual(value2, argument.Value) self.assertListEqual([f"-{name}", f"{value2}"], list(argument.AsArgument())) self.assertEqual(f"\"-{name}\" \"{value2}\"", str(argument)) self.assertEqual(f"\"-{name}\", \"{value2}\"", repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_LongTupleFlag(self) -> None: with self.assertRaises(TypeError): _ = LongTupleFlag() def test_DerivedLongTupleFlag(self) -> None: name = "flag" value = "42" class Flag(LongTupleFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertEqual(value, argument.Value) self.assertListEqual([f"--{name}", f"{value}"], list(argument.AsArgument())) self.assertEqual(f"\"--{name}\" \"{value}\"", str(argument)) self.assertEqual(f"\"--{name}\", \"{value}\"", repr(argument)) # with self.assertRaises(TypeError): # argument.Value = 42 value2 = "84" argument.Value = value2 self.assertEqual(value2, argument.Value) self.assertListEqual([f"--{name}", f"{value2}"], list(argument.AsArgument())) self.assertEqual(f"\"--{name}\" \"{value2}\"", str(argument)) self.assertEqual(f"\"--{name}\", \"{value2}\"", repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" def test_WindowsTupleFlag(self) -> None: with self.assertRaises(TypeError): _ = WindowsTupleFlag() def test_DerivedWindowsTupleFlag(self) -> None: name = "flag" value = "42" class Flag(WindowsTupleFlag, name=name): pass argument = Flag(value) self.assertIs(name, argument.Name) self.assertEqual(value, argument.Value) self.assertListEqual([f"/{name}", f"{value}"], list(argument.AsArgument())) self.assertEqual(f"\"/{name}\" \"{value}\"", str(argument)) self.assertEqual(f"\"/{name}\", \"{value}\"", repr(argument)) # with self.assertRaises(TypeError): # argument.Value = 42 value2 = "84" argument.Value = value2 self.assertEqual(value2, argument.Value) self.assertListEqual([f"/{name}", f"{value2}"], list(argument.AsArgument())) self.assertEqual(f"\"/{name}\" \"{value2}\"", str(argument)) self.assertEqual(f"\"/{name}\", \"{value2}\"", repr(argument)) with self.assertRaises(AttributeError): argument.Name = "flag2" class KeyValueFlags(TestCase): def test_KeyValueFlag(self) -> None: with self.assertRaises(TypeError): _ = NamedKeyValuePairsArgument() def test_DerivedNamedKeyValuePairsArgument(self) -> None: name = "g" pairs = {"key1": "value1", "key2": "value2"} class Flag(NamedKeyValuePairsArgument, name=name): pass argument = Flag(pairs) self.assertIs(name, argument.Name) self.assertDictEqual(pairs, argument.Value) self.assertListEqual([f"{name}{key}={value}" for key, value in pairs.items()], list(argument.AsArgument())) # TODO: should property Value check for a dictionary type and raise a TypeError? with self.assertRaises(AttributeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = {42: "value42"} with self.assertRaises(TypeError): argument.Value = {"key84": 84} pairs2 = {"key3": "value3", "key4": "value4"} argument.Value = pairs2 self.assertIs(name, argument.Name) self.assertDictEqual(pairs2, argument.Value) self.assertListEqual([f"{name}{key}={value}" for key, value in pairs2.items()], list(argument.AsArgument())) with self.assertRaises(AttributeError): argument.Name = "G" def test_ShortKeyValueFlag(self) -> None: with self.assertRaises(TypeError): _ = ShortKeyValueFlag() def test_DerivedShortKeyValueFlag(self) -> None: name = "g" pairs = {"key1": "value1", "key2": "value2"} class Flag(ShortKeyValueFlag, name=name): pass argument = Flag(pairs) self.assertIs(name, argument.Name) self.assertDictEqual(pairs, argument.Value) self.assertListEqual([f"-{name}{key}={value}" for key, value in pairs.items()], list(argument.AsArgument())) # TODO: should property Value check for a dictionary type and raise a TypeError? with self.assertRaises(AttributeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = {42: "value42"} with self.assertRaises(TypeError): argument.Value = {"key84": 84} pairs2 = {"key3": "value3", "key4": "value4"} argument.Value = pairs2 self.assertIs(name, argument.Name) self.assertDictEqual(pairs2, argument.Value) self.assertListEqual([f"-{name}{key}={value}" for key, value in pairs2.items()], list(argument.AsArgument())) with self.assertRaises(AttributeError): argument.Name = "G" def test_LongKeyValueFlag(self) -> None: with self.assertRaises(TypeError): _ = LongKeyValueFlag() def test_DerivedLongKeyValueFlag(self) -> None: name = "g" pairs = {"key1": "value1", "key2": "value2"} class Flag(LongKeyValueFlag, name=name): pass argument = Flag(pairs) self.assertIs(name, argument.Name) self.assertDictEqual(pairs, argument.Value) self.assertListEqual([f"--{name}{key}={value}" for key, value in pairs.items()], list(argument.AsArgument())) # TODO: should property Value check for a dictionary type and raise a TypeError? with self.assertRaises(AttributeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = {42: "value42"} with self.assertRaises(TypeError): argument.Value = {"key84": 84} pairs2 = {"key3": "value3", "key4": "value4"} argument.Value = pairs2 self.assertIs(name, argument.Name) self.assertDictEqual(pairs2, argument.Value) self.assertListEqual([f"--{name}{key}={value}" for key, value in pairs2.items()], list(argument.AsArgument())) with self.assertRaises(AttributeError): argument.Name = "G" def test_WindowsKeyValueFlag(self) -> None: with self.assertRaises(TypeError): _ = WindowsKeyValueFlag() def test_DerivedWindowsKeyValueFlag(self) -> None: name = "g" pairs = {"key1": "value1", "key2": "value2"} class Flag(WindowsKeyValueFlag, name=name): pass argument = Flag(pairs) self.assertIs(name, argument.Name) self.assertDictEqual(pairs, argument.Value) self.assertListEqual([f"/{name}:{key}={value}" for key, value in pairs.items()], list(argument.AsArgument())) # TODO: should property Value check for a dictionary type and raise a TypeError? with self.assertRaises(AttributeError): argument.Value = 42 with self.assertRaises(TypeError): argument.Value = {42: "value42"} with self.assertRaises(TypeError): argument.Value = {"key84": 84} pairs2 = {"key3": "value3", "key4": "value4"} argument.Value = pairs2 self.assertIs(name, argument.Name) self.assertDictEqual(pairs2, argument.Value) self.assertListEqual([f"/{name}:{key}={value}" for key, value in pairs2.items()], list(argument.AsArgument())) with self.assertRaises(AttributeError): argument.Name = "G" pyTooling-8.11.0/tests/unit/CLIAbstraction/Environment.py000066400000000000000000000105771513317154500234040ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Testcase for operating system program ``mkdir``. :copyright: Copyright 2007-2026 Patrick Lehmann - Bötzingen, Germany :license: Apache License, Version 2.0 """ from unittest import TestCase from pyTooling.CLIAbstraction import Environment if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class DefaultEnvironment(TestCase): def test_Empty(self) -> None: env = Environment(newVariables={}) self.assertEqual(0, len(env)) self.assertFalse("PATH" in env) def test_SystemDefault(self) -> None: env = Environment() self.assertTrue(len(env) > 10) self.assertTrue("PATH" in env) class ArtificialEnvironment(TestCase): def test_Simple(self) -> None: variables = {"PATH": "/bin"} env = Environment(newVariables=variables) self.assertTrue("PATH" in env) self.assertEqual("/bin", env["PATH"]) pyTooling-8.11.0/tests/unit/CLIAbstraction/Examples.py000066400000000000000000000114631513317154500226510ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Abstracted CLI programs as examples for unit tests. :copyright: Copyright 2007-2026 Patrick Lehmann - Bötzingen, Germany :license: Apache License, Version 2.0 """ from pyTooling.MetaClasses import ExtendedType from pyTooling.CLIAbstraction import CLIArgument from pyTooling.CLIAbstraction.Command import CommandArgument from pyTooling.CLIAbstraction.Flag import LongFlag from pyTooling.CLIAbstraction.ValuedTupleFlag import ShortTupleFlag if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class GitArgumentsMixin(metaclass=ExtendedType, mixin=True): # _executableNames: ClassVar[Dict[str, str]] = { _executableNames = { "Darwin": "git", "FreeBSD": "git", "Linux": "git", "Windows": "git.exe" } @CLIArgument() class FlagVersion(LongFlag, name="version"): ... @CLIArgument() class FlagHelp(LongFlag, name="help"): ... @CLIArgument() class CommandHelp(CommandArgument, name="help"): ... @CLIArgument() class CommandInit(CommandArgument, name="init"): ... @CLIArgument() class CommandStage(CommandArgument, name="add"): ... @CLIArgument() class CommandCommit(CommandArgument, name="commit"): ... @CLIArgument() class ValueCommitMessage(ShortTupleFlag, name="m"): ... pyTooling-8.11.0/tests/unit/CLIAbstraction/Executable.py000066400000000000000000000162431513317154500231550ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Testcase for operating system program ``mkdir``. :copyright: Copyright 2007-2026 Patrick Lehmann - Bötzingen, Germany :license: Apache License, Version 2.0 """ from pathlib import Path from typing import Any, Self from pytest import mark from sys import platform as sys_platform from unittest import TestCase from pyTooling.CLIAbstraction import Executable, Environment from . import Helper from .Examples import GitArgumentsMixin if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Git(Executable, GitArgumentsMixin): def __new__(cls, *args: Any, **kwargs: Any) -> Self: cls._executableNames = { "Darwin": "git", "FreeBSD": "git", "Linux": "git", "Windows": "git.exe" } return super().__new__(cls) @mark.skipif(sys_platform in ("darwin", "linux", "win32"), reason="Don't run these tests on Linux, macOS and Windows.") class ExplicitBinaryDirectoryOnFreeBSD(TestCase, Helper): _binaryDirectoryPath = Path("/usr/local/bin") def test_VersionFlag(self) -> None: tool = Git(binaryDirectoryPath=self._binaryDirectoryPath) tool[tool.FlagVersion] = True tool.StartProcess(environment=Environment(addVariables={"LANGUAGE": "C"})) output = "\n".join(tool.GetLineReader()) self.assertRegex(output, r"git version \d+.\d+.\d+") @mark.skipif(sys_platform in ("freebsd", "win32"), reason="Don't run these tests on FreeBSD and Windows.") class ExplicitBinaryDirectoryOnLinux(TestCase, Helper): _binaryDirectoryPath = Path("/usr/bin") def test_VersionFlag(self) -> None: tool = Git(binaryDirectoryPath=self._binaryDirectoryPath) tool[tool.FlagVersion] = True tool.StartProcess(environment=Environment(addVariables={"LANGUAGE": "C"})) output = "\n".join(tool.GetLineReader()) self.assertRegex(output, r"git version \d+.\d+.\d+") @mark.skipif(sys_platform in ("darwin", "freebsd", "linux"), reason="Don't run these tests on FreeBSD, Linux or macOS.") class ExplicitBinaryDirectoryOnWindows(TestCase, Helper): _binaryDirectoryPath = Path(r"C:\Program Files\Git\cmd") def test_VersionFlag(self) -> None: tool = Git(binaryDirectoryPath=self._binaryDirectoryPath) tool[tool.FlagVersion] = True tool.StartProcess(environment=Environment(addVariables={"LANGUAGE": "C"})) output = "\n".join(tool.GetLineReader()) self.assertRegex(output, r"git version \d+.\d+.\d+.windows.\d+") class CommonOptions(TestCase, Helper): def test_VersionFlag(self) -> None: print() tool = Git() tool[tool.FlagVersion] = True tool.StartProcess(environment=Environment(addVariables={"LANGUAGE": "C"})) output = "\n".join(tool.GetLineReader()) self.assertRegex(output, r"git version \d+.\d+.\d+(.windows.\d+)?") print(output) def test_HelpFlag(self) -> None: print() tool = Git() tool[tool.FlagHelp] = True tool.StartProcess(environment=Environment(addVariables={"LANGUAGE": "C"})) output = "\n".join(tool.GetLineReader()) self.assertRegex(output, r"^usage: git") print(output) def test_HelpCommand(self) -> None: print() tool = Git() tool[tool.CommandHelp] = True tool.StartProcess(environment=Environment(addVariables={"LANGUAGE": "C"})) output = "\n".join(tool.GetLineReader()) self.assertRegex(output, r"^usage: git") print(output) # class Commit(TestCase, Helper): # def test_CommitWithMessage(self) -> None: # tool = Git() # tool[tool.CommandCommit] = True # tool[tool.ValueCommitMessage] = "Initial commit." # # executable = self.GetExecutablePath("git") # tool.StartProcess() pyTooling-8.11.0/tests/unit/CLIAbstraction/Program.py000066400000000000000000000271061513317154500225030ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Testcase for operating system program ``mkdir``. :copyright: Copyright 2007-2026 Patrick Lehmann - Bötzingen, Germany :license: Apache License, Version 2.0 """ from pathlib import Path from typing import Any, Self from pytest import mark from sys import platform as sys_platform from unittest import TestCase from pyTooling.CLIAbstraction import Program, CLIAbstractionException, CLIArgument from pyTooling.CLIAbstraction.Flag import LongFlag from . import Helper from .Examples import GitArgumentsMixin if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Git(Program, GitArgumentsMixin): def __new__(cls, *args: Any, **kwargs: Any) -> Self: cls._executableNames = { "Darwin": "git", "FreeBSD": "git", "Linux": "git", "Windows": "git.exe" } return super().__new__(cls) class Gitt(Program): _executableNames = { "Darwin": "gitt", "FreeBSD": "gitt", "Linux": "gitt", "Windows": "gitt.exe" } @CLIArgument() class FlagVersion(LongFlag, name="version"): ... class GitUnknownOS(Program): _executableNames = { "UnknownOS": "git" } @mark.skipif(sys_platform in ("darwin", "linux", "win32"), reason="Don't run these tests on Linux, macOS and Windows.") class ExplicitPathsOnFreeBSD(TestCase, Helper): _binaryDirectoryPath = Path("/usr/local/bin") def test_BinaryDirectory(self) -> None: tool = Git(binaryDirectoryPath=self._binaryDirectoryPath) executable = self.GetExecutablePath("git", self._binaryDirectoryPath) self.assertEqual(Path(executable), tool.Path) self.assertListEqual([executable], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\"]", repr(tool)) self.assertEqual(f"\"{executable}\"", str(tool)) def test_BinaryDirectory_NotAPath(self) -> None: with self.assertRaises(TypeError): _ = Git(binaryDirectoryPath=str(self._binaryDirectoryPath)) def test_BinaryDirectory_DoesNotExist(self) -> None: with self.assertRaises(CLIAbstractionException): _ = Git(binaryDirectoryPath=self._binaryDirectoryPath / "git") def test_ExecutablePath(self) -> None: tool = Git(executablePath=self._binaryDirectoryPath / "git") executable = self.GetExecutablePath("git", self._binaryDirectoryPath) self.assertEqual(Path(executable), tool.Path) self.assertListEqual([executable], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\"]", repr(tool)) self.assertEqual(f"\"{executable}\"", str(tool)) def test_ExecutablePath_NotAPath(self) -> None: with self.assertRaises(TypeError): _ = Git(executablePath=str(self._binaryDirectoryPath / "git")) def test_ExecutablePath_DoesNotExist(self) -> None: with self.assertRaises(CLIAbstractionException): _ = Git(executablePath=self._binaryDirectoryPath / "gitt") @mark.skipif(sys_platform in ("freebsd", "win32"), reason="Don't run these tests on FreeBSD and Windows.") class ExplicitPathsOnLinux(TestCase, Helper): _binaryDirectoryPath = Path("/usr/bin") def test_BinaryDirectory(self) -> None: tool = Git(binaryDirectoryPath=self._binaryDirectoryPath) executable = self.GetExecutablePath("git", self._binaryDirectoryPath) self.assertEqual(Path(executable), tool.Path) self.assertListEqual([executable], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\"]", repr(tool)) self.assertEqual(f"\"{executable}\"", str(tool)) def test_BinaryDirectory_NotAPath(self) -> None: with self.assertRaises(TypeError): _ = Git(binaryDirectoryPath=str(self._binaryDirectoryPath)) def test_BinaryDirectory_DoesNotExist(self) -> None: with self.assertRaises(CLIAbstractionException): _ = Git(binaryDirectoryPath=self._binaryDirectoryPath / "git") def test_ExecutablePath(self) -> None: tool = Git(executablePath=self._binaryDirectoryPath / "git") executable = self.GetExecutablePath("git", self._binaryDirectoryPath) self.assertEqual(Path(executable), tool.Path) self.assertListEqual([executable], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\"]", repr(tool)) self.assertEqual(f"\"{executable}\"", str(tool)) def test_ExecutablePath_NotAPath(self) -> None: with self.assertRaises(TypeError): _ = Git(executablePath=str(self._binaryDirectoryPath / "git")) def test_ExecutablePath_DoesNotExist(self) -> None: with self.assertRaises(CLIAbstractionException): _ = Git(executablePath=self._binaryDirectoryPath / "gitt") @mark.skipif(sys_platform in ("darwin", "freebsd", "linux"), reason="Don't run these tests on FreeBSD, Linux or macOS.") class ExplicitPathsOnWindows(TestCase, Helper): _binaryDirectoryPath = Path(r"C:\Program Files\Git\cmd") def test_BinaryDirectory(self) -> None: tool = Git(binaryDirectoryPath=self._binaryDirectoryPath) executable = self.GetExecutablePath("git", self._binaryDirectoryPath) self.assertEqual(Path(executable), tool.Path) self.assertListEqual([executable], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\"]", repr(tool)) self.assertEqual(f"\"{executable}\"", str(tool)) def test_BinaryDirectory_NotAPath(self) -> None: with self.assertRaises(TypeError): _ = Git(binaryDirectoryPath=str(self._binaryDirectoryPath)) def test_BinaryDirectory_DoesNotExist(self) -> None: with self.assertRaises(CLIAbstractionException): _ = Git(binaryDirectoryPath=self._binaryDirectoryPath / "git") def test_ExecutablePath(self) -> None: tool = Git(executablePath=self._binaryDirectoryPath / "git.exe") executable = self.GetExecutablePath("git", self._binaryDirectoryPath) self.assertEqual(Path(executable), tool.Path) self.assertListEqual([executable], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\"]", repr(tool)) self.assertEqual(f"\"{executable}\"", str(tool)) def test_ExecutablePath_NotAPath(self) -> None: with self.assertRaises(TypeError): _ = Git(executablePath=str(self._binaryDirectoryPath / "git.exe")) def test_ExecutablePath_DoesNotExist(self) -> None: with self.assertRaises(CLIAbstractionException): _ = Git(executablePath=self._binaryDirectoryPath / "gitt.exe") class CommonOptions(TestCase, Helper): def test_UnknownOS(self) -> None: with self.assertRaises(CLIAbstractionException): _ = GitUnknownOS() def test_BinaryDirectory_UnknownOS(self) -> None: with self.assertRaises(CLIAbstractionException): _ = GitUnknownOS(binaryDirectoryPath=Path("")) def test_NotInPath(self) -> None: with self.assertRaises(CLIAbstractionException): _ = Gitt() def test_SetUnknownFlag(self) -> None: tool = Git() with self.assertRaises(TypeError): tool["version"] = True with self.assertRaises(KeyError): tool[Gitt.FlagVersion] = True tool[tool.FlagVersion] = True with self.assertRaises(KeyError): tool[tool.FlagVersion] = True def test_GetUnknownFlag(self) -> None: tool = Git() with self.assertRaises(KeyError): _ = tool[tool.FlagVersion] tool[tool.FlagVersion] = True with self.assertRaises(TypeError): _ = tool["version"] def test_VersionFlag(self) -> None: tool = Git() tool[tool.FlagVersion] = True executable = self.GetExecutablePath("git") self.assertListEqual([executable, "--version"], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\", \"--version\"]", repr(tool)) def test_HelpFlag(self) -> None: tool = Git() tool[tool.FlagHelp] = True executable = self.GetExecutablePath("git") self.assertListEqual([executable, "--help"], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\", \"--help\"]", repr(tool)) def test_HelpCommand(self) -> None: tool = Git() tool[tool.CommandHelp] = True executable = self.GetExecutablePath("git") self.assertListEqual([executable, "help"], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\", \"help\"]", repr(tool)) class Commit(TestCase, Helper): def test_CommitWithMessage(self) -> None: tool = Git() tool[tool.CommandCommit] = True tool[tool.ValueCommitMessage] = "Initial commit." executable = self.GetExecutablePath("git") self.assertListEqual([executable, "commit", "-m", "Initial commit."], tool.ToArgumentList()) self.assertEqual(f"[\"{executable}\", \"commit\", \"-m\", \"Initial commit.\"]", repr(tool)) pyTooling-8.11.0/tests/unit/CLIAbstraction/__init__.py000066400000000000000000000076421513317154500226360ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ___ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Helper classes for unit tests.""" from pathlib import Path from platform import system from typing import Optional as Nullable class Helper: _system = system() @classmethod def GetExecutablePath(cls, programName: str, binaryDirectory: Nullable[Path] = None) -> str: extensions = ".exe" if cls._system == "Windows" else "" programName = f"{programName}{extensions}" if binaryDirectory is not None: return str(binaryDirectory / programName) else: return programName pyTooling-8.11.0/tests/unit/CallByRef/000077500000000000000000000000001513317154500175165ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/CallByRef/CallByRef.py000066400000000000000000000231211513317154500216720ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ ____ ____ __ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _| | | __ ) _ _| _ \ ___ / _| # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | | | _ \| | | | |_) / _ \ |_ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | |_) | |_| | _ < __/ _| # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_|_|____/ \__, |_| \_\___|_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for :mod:`PyTooling.CallByRef`. """ from unittest import TestCase from pyTooling.CallByRef import CallByRefParam, CallByRefBoolParam, CallByRefIntParam if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) def func1(param: CallByRefParam) -> None: param <<= (3, 4) def func2(param: CallByRefBoolParam, value: bool = True) -> None: param <<= value def assign_42(param: CallByRefIntParam, value: int = 42) -> None: param <<= value class Any(TestCase): ref: CallByRefParam = CallByRefParam() def setUp(self) -> None: func1(self.ref) def test_Value(self) -> None: self.assertTupleEqual((3, 4), self.ref.Value) def test_Equal(self) -> None: self.assertTrue(self.ref == (3, 4)) def test_Unequal(self) -> None: self.assertTrue(self.ref != (4, 3)) class Boolean(TestCase): ref: CallByRefBoolParam = CallByRefBoolParam() def setUp(self) -> None: func2(self.ref) def test_Value(self) -> None: self.assertTrue(self.ref.Value) def test_Equal(self) -> None: self.assertTrue(self.ref == True) with self.assertRaises(TypeError): _ = self.ref == "str" def test_Unequal(self) -> None: self.assertTrue(self.ref != False) with self.assertRaises(TypeError): _ = self.ref != "str" def test_TypeConvertToBool(self) -> None: self.assertTrue(bool(self.ref)) def test_TypeConvertToInt(self) -> None: self.assertEqual(1, int(self.ref)) class Integer(TestCase): ref: CallByRefIntParam = CallByRefIntParam() def test_Value(self) -> None: assign_42(self.ref) self.assertEqual(42, self.ref.Value) def test_Negate(self) -> None: assign_42(self.ref) self.assertEqual(-42, -self.ref) def test_Positive(self) -> None: assign_42(self.ref, -42) self.assertEqual(-42, +self.ref) def test_Invert(self) -> None: assign_42(self.ref, 1) self.assertEqual(-2, ~self.ref) def test_GeaterThanOrEqual(self) -> None: assign_42(self.ref) self.assertTrue(self.ref >= 40) with self.assertRaises(TypeError): _ = self.ref >= "str" def test_GreaterThan(self) -> None: assign_42(self.ref) self.assertTrue(self.ref > 41) with self.assertRaises(TypeError): _ = self.ref > "str" def test_Equal(self) -> None: assign_42(self.ref) self.assertTrue(self.ref == 42) with self.assertRaises(TypeError): _ = self.ref == "str" def test_Unequal(self) -> None: assign_42(self.ref) self.assertTrue(self.ref != 43) with self.assertRaises(TypeError): _ = self.ref != "str" def test_LessThan(self) -> None: assign_42(self.ref) self.assertTrue(self.ref < 44) with self.assertRaises(TypeError): _ = self.ref < "str" def test_LessThanOrEqual(self) -> None: assign_42(self.ref) self.assertTrue(self.ref <= 45) with self.assertRaises(TypeError): _ = self.ref <= "str" def test_Addition(self) -> None: assign_42(self.ref) self.assertEqual(43, self.ref + 1) with self.assertRaises(TypeError): self.ref + "str" def test_Subtraction(self) -> None: assign_42(self.ref) self.assertEqual(41, self.ref - 1) with self.assertRaises(TypeError): self.ref - "str" def test_Multiplication(self) -> None: assign_42(self.ref) self.assertEqual(42, self.ref * 1) with self.assertRaises(TypeError): self.ref * "str" def test_Power(self) -> None: assign_42(self.ref) self.assertEqual(42, self.ref ** 1) with self.assertRaises(TypeError): self.ref ** "str" def test_Division(self) -> None: assign_42(self.ref) self.assertEqual(42, self.ref / 1) with self.assertRaises(TypeError): self.ref / "str" def test_FloorDivision(self) -> None: assign_42(self.ref) self.assertEqual(42, self.ref // 1) with self.assertRaises(TypeError): self.ref // "str" def test_Modulo(self) -> None: assign_42(self.ref) self.assertEqual(0, self.ref % 2) with self.assertRaises(TypeError): self.ref % "str" def test_And(self) -> None: assign_42(self.ref) self.assertEqual(2, self.ref & 2) with self.assertRaises(TypeError): self.ref & "str" def test_Or(self) -> None: assign_42(self.ref) self.assertEqual(43, self.ref | 1) with self.assertRaises(TypeError): self.ref | "str" def test_Xor(self) -> None: assign_42(self.ref) self.assertEqual(40, self.ref ^ 2) with self.assertRaises(TypeError): self.ref ^ "str" def test_Increment(self) -> None: assign_42(self.ref) self.ref += 1 self.assertEqual(43, self.ref) with self.assertRaises(TypeError): self.ref += "str" def test_Decrement(self) -> None: assign_42(self.ref) self.ref -= 1 self.assertEqual(41, self.ref) with self.assertRaises(TypeError): self.ref -= "str" def test_InplaceMultiplication(self) -> None: assign_42(self.ref) self.ref *= 1 self.assertEqual(42, self.ref) with self.assertRaises(TypeError): self.ref *= "str" def test_InplacePower(self) -> None: assign_42(self.ref) self.ref **= 1 self.assertEqual(42, self.ref) with self.assertRaises(TypeError): self.ref **= "str" def test_InplaceDivision(self) -> None: assign_42(self.ref) self.ref /= 1 self.assertEqual(42, self.ref) with self.assertRaises(TypeError): self.ref /= "str" def test_InplaceFloorDivision(self) -> None: assign_42(self.ref) self.ref //= 1 self.assertEqual(42, self.ref) with self.assertRaises(TypeError): self.ref //= "str" def test_InplaceModulo(self) -> None: assign_42(self.ref) self.ref %= 2 self.assertEqual(0, self.ref) with self.assertRaises(TypeError): self.ref %= "str" def test_InplaceAnd(self) -> None: assign_42(self.ref) self.ref &= 2 self.assertEqual(2, self.ref) with self.assertRaises(TypeError): self.ref &= "str" def test_InplaceOr(self) -> None: assign_42(self.ref) self.ref |= 1 self.assertEqual(43, self.ref) with self.assertRaises(TypeError): self.ref |= "str" def test_InplaceXor(self) -> None: assign_42(self.ref) self.ref ^= 2 self.assertEqual(40, self.ref) with self.assertRaises(TypeError): self.ref ^= "str" def test_TypeConvertToBool(self) -> None: self.assertTrue(bool(self.ref)) def test_TypeConvertToInt(self) -> None: self.assertEqual(42, int(self.ref)) pyTooling-8.11.0/tests/unit/Cartesian/000077500000000000000000000000001513317154500176245ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Cartesian/Cartesian2D.py000066400000000000000000000336741513317154500223120ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ ____ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ \| _ \ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ __) | | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |/ __/| |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|_____|____/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for ... """ from math import sqrt from unittest import TestCase from pyTooling.Cartesian2D import Origin2D, Point2D, Offset2D, Size2D, LineSegment2D if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Origin(self) -> None: origin = Origin2D() self.assertIsInstance(origin.x, int) self.assertIsInstance(origin.y, int) self.assertEqual(0, origin.x) self.assertEqual(0, origin.y) self.assertTupleEqual((0, 0), origin.ToTuple()) self.assertEqual("Origin2D(0, 0)", repr(origin)) def test_Point_int(self) -> None: point = Point2D(1, 2) self.assertIsInstance(point.x, int) self.assertIsInstance(point.y, int) self.assertEqual(1, point.x) self.assertEqual(2, point.y) self.assertTupleEqual((1, 2), point.ToTuple()) self.assertEqual("Point2D(1, 2)", repr(point)) def test_Point_float(self) -> None: point = Point2D(1.0, 2.0) self.assertIsInstance(point.x, float) self.assertIsInstance(point.y, float) self.assertEqual(1.0, point.x) self.assertEqual(2.0, point.y) self.assertTupleEqual((1.0, 2.0), point.ToTuple()) self.assertEqual("Point2D(1.0, 2.0)", repr(point)) def test_Point_str1(self) -> None: with self.assertRaises(TypeError): _ =Point2D("1", 2) def test_Point_str2(self) -> None: with self.assertRaises(TypeError): _ =Point2D(1, "2") def test_Offset_int(self) -> None: offset = Offset2D(1, 2) self.assertIsInstance(offset.xOffset, int) self.assertIsInstance(offset.yOffset, int) self.assertEqual(1, offset.xOffset) self.assertEqual(2, offset.yOffset) self.assertTupleEqual((1, 2), offset.ToTuple()) self.assertEqual("Offset2D(1, 2)", repr(offset)) def test_Offset_float(self) -> None: offset = Offset2D(1.0, 2.0) self.assertIsInstance(offset.xOffset, float) self.assertIsInstance(offset.yOffset, float) self.assertEqual(1.0, offset.xOffset) self.assertEqual(2.0, offset.yOffset) self.assertTupleEqual((1.0, 2.0), offset.ToTuple()) self.assertEqual("Offset2D(1.0, 2.0)", repr(offset)) def test_Offset_str1(self) -> None: with self.assertRaises(TypeError): _ = Offset2D("1", 2) def test_Offset_str2(self) -> None: with self.assertRaises(TypeError): _ = Offset2D(1, "2") def test_Size_int(self) -> None: size = Size2D(1, 2) self.assertIsInstance(size.width, int) self.assertIsInstance(size.height, int) self.assertEqual(1, size.width) self.assertEqual(2, size.height) self.assertTupleEqual((1, 2), size.ToTuple()) self.assertEqual("Size2D(1, 2)", repr(size)) def test_Size_float(self) -> None: size = Size2D(1.0, 2.0) self.assertIsInstance(size.width, float) self.assertIsInstance(size.height, float) self.assertEqual(1.0, size.width) self.assertEqual(2.0, size.height) self.assertTupleEqual((1.0, 2.0), size.ToTuple()) self.assertEqual("Size2D(1.0, 2.0)", repr(size)) def test_Size_str1(self) -> None: with self.assertRaises(TypeError): _ = Size2D("1", 2) def test_Size_str2(self) -> None: with self.assertRaises(TypeError): _ = Size2D(1, "2") def test_LineSegment(self) -> None: point1 = Point2D(1, 2) point2 = Point2D(2, 3) line = LineSegment2D(point1, point2) offset = Offset2D(1, 1) self.assertEqual(sqrt(2), line.Length) self.assertEqual(offset, line.ToOffset()) self.assertTupleEqual(((1, 2), (2, 3)), line.ToTuple()) class Copy(TestCase): def test_Origin(self) -> None: origin = Origin2D() with self.assertRaises(RuntimeError): _ = origin.Copy() def test_Point_int(self) -> None: point = Point2D(1, 2) newPoint = point.Copy() self.assertIsInstance(newPoint.x, int) self.assertIsInstance(newPoint.y, int) self.assertEqual(1, newPoint.x) self.assertEqual(2, newPoint.y) self.assertTupleEqual((1, 2), newPoint.ToTuple()) self.assertEqual("Point2D(1, 2)", repr(newPoint)) def test_Point_float(self) -> None: point = Point2D(1.0, 2.0) newPoint = point.Copy() self.assertIsInstance(newPoint.x, float) self.assertIsInstance(newPoint.y, float) self.assertEqual(1.0, newPoint.x) self.assertEqual(2.0, newPoint.y) self.assertTupleEqual((1.0, 2.0), newPoint.ToTuple()) self.assertEqual("Point2D(1.0, 2.0)", repr(newPoint)) def test_Offset_int(self) -> None: offset = Offset2D(1, 2) newOffset = offset.Copy() self.assertIsInstance(newOffset.xOffset, int) self.assertIsInstance(newOffset.yOffset, int) self.assertEqual(1, newOffset.xOffset) self.assertEqual(2, newOffset.yOffset) self.assertTupleEqual((1, 2), newOffset.ToTuple()) self.assertEqual("Offset2D(1, 2)", repr(newOffset)) def test_Offset_float(self) -> None: offset = Offset2D(1.0, 2.0) newOffset = offset.Copy() self.assertIsInstance(newOffset.xOffset, float) self.assertIsInstance(newOffset.yOffset, float) self.assertEqual(1.0, newOffset.xOffset) self.assertEqual(2.0, newOffset.yOffset) self.assertTupleEqual((1.0, 2.0), newOffset.ToTuple()) self.assertEqual("Offset2D(1.0, 2.0)", repr(newOffset)) def test_Size_int(self) -> None: size = Size2D(1, 2) newSize = size.Copy() self.assertIsInstance(newSize.width, int) self.assertIsInstance(newSize.height, int) self.assertEqual(1, newSize.width) self.assertEqual(2, newSize.height) self.assertTupleEqual((1, 2), newSize.ToTuple()) self.assertEqual("Size2D(1, 2)", repr(newSize)) def test_Size_float(self) -> None: size = Size2D(1.0, 2.0) newSize = size.Copy() self.assertIsInstance(newSize.width, float) self.assertIsInstance(newSize.height, float) self.assertEqual(1.0, newSize.width) self.assertEqual(2.0, newSize.height) self.assertTupleEqual((1.0, 2.0), newSize.ToTuple()) self.assertEqual("Size2D(1.0, 2.0)", repr(newSize)) class Comparison(TestCase): def test_Offset_Equal_Offset(self) -> None: offset1 = Offset2D(1, 2) offset2 = Offset2D(1, 2) self.assertEqual(offset1, offset2) def test_Offset_Equal_Tuple(self) -> None: offset1 = Offset2D(1, 2) offset2 = (1, 2) self.assertEqual(offset1, offset2) def test_Offset_Equal_int(self) -> None: offset1 = Offset2D(1, 2) offset2 = 2 with self.assertRaises(TypeError): _ = offset1 == offset2 def test_Offset_Unequal_Offset(self) -> None: offset1 = Offset2D(1, 2) offset2 = Offset2D(2, 3) self.assertTrue(offset1 != offset2) class PointArithmetic(TestCase): def test_Point_Plus_Point(self) -> None: point1 = Point2D(1, 2) point2 = Point2D(2, 3) with self.assertRaises(TypeError): _ = point1 + point2 def test_Point_Plus_Offset(self) -> None: point = Point2D(1, 2) offset = Offset2D(2, 3) newPoint = point + offset self.assertEqual(3, newPoint.x) self.assertEqual(5, newPoint.y) def test_Point_Plus_Tuple(self) -> None: point = Point2D(1, 2) offset = (2, 3) newPoint = point + offset self.assertEqual(3, newPoint.x) self.assertEqual(5, newPoint.y) def test_Point_InplacePlus_Offset(self) -> None: point = Point2D(1, 2) offset = Offset2D(2, 3) point += offset self.assertEqual(3, point.x) self.assertEqual(5, point.y) def test_Point_InplacePlus_Tuple(self) -> None: point = Point2D(1, 2) offset = (2, 3) point += offset self.assertEqual(3, point.x) self.assertEqual(5, point.y) def test_Point_InplacePlus_Int(self) -> None: point = Point2D(1, 2) with self.assertRaises(TypeError): point += 2 def test_Point_Minus_Offset(self) -> None: point = Point2D(1, 2) offset = Offset2D(2, 3) newPoint = point + -offset self.assertEqual(-1, newPoint.x) self.assertEqual(-1, newPoint.y) def test_Point_Minus_Point(self) -> None: point1 = Point2D(1, 2) point2 = Point2D(2, 3) offset = point2 - point1 self.assertEqual(1, offset.xOffset) self.assertEqual(1, offset.yOffset) def test_Point_Minus_Tuple(self) -> None: point = Point2D(1, 2) with self.assertRaises(TypeError): _ = point - 2 def test_Point_InplaceMinus_Offset(self) -> None: point = Point2D(1, 2) offset = Offset2D(2, 3) point -= offset self.assertEqual(-1, point.x) self.assertEqual(-1, point.y) def test_Point_InplaceMinus_Tuple(self) -> None: point = Point2D(1, 2) offset = (2, 3) point -= offset self.assertEqual(-1, point.x) self.assertEqual(-1, point.y) def test_Point_InplaceMinus_Int(self) -> None: point = Point2D(1, 2) with self.assertRaises(TypeError): point -= 2 class OffsetArithmetic(TestCase): def test_Offset_Plus_Offset(self) -> None: offset1 = Offset2D(1, 2) offset2 = Offset2D(2, 3) newOffset = offset1 + offset2 self.assertEqual(3, newOffset.xOffset) self.assertEqual(5, newOffset.yOffset) def test_Offset_Plus_Tuple(self) -> None: offset1 = Offset2D(1, 2) offset2 = (2, 3) newOffset = offset1 + offset2 self.assertEqual(3, newOffset.xOffset) self.assertEqual(5, newOffset.yOffset) def test_Offset_Plus_int(self) -> None: offset1 = Offset2D(1, 2) offset2 = 2 with self.assertRaises(TypeError): _ = offset1 + offset2 def test_Offset_InplacePlus_Offset(self) -> None: offset1 = Offset2D(1, 2) offset2 = Offset2D(2, 3) offset1 += offset2 self.assertEqual(3, offset1.xOffset) self.assertEqual(5, offset1.yOffset) def test_Offset_InplacePlus_Tuple(self) -> None: offset1 = Offset2D(1, 2) offset2 = (2, 3) offset1 += offset2 self.assertEqual(3, offset1.xOffset) self.assertEqual(5, offset1.yOffset) def test_Offset_InplacePlus_Int(self) -> None: offset = Offset2D(1, 2) with self.assertRaises(TypeError): offset += 2 def test_Offset_Minus_Offset(self) -> None: offset1 = Offset2D(1, 2) offset2 = Offset2D(2, 3) newOffset = offset1 - offset2 self.assertEqual(-1, newOffset.xOffset) self.assertEqual(-1, newOffset.yOffset) def test_Offset_Minus_Tuple(self) -> None: offset1 = Offset2D(1, 2) offset2 = (2, 3) newOffset = offset1 - offset2 self.assertEqual(-1, newOffset.xOffset) self.assertEqual(-1, newOffset.yOffset) def test_Offset_Minus_int(self) -> None: offset1 = Offset2D(1, 2) offset2 = 2 with self.assertRaises(TypeError): _ = offset1 - offset2 def test_Offset_InplaceMinus_Offset(self) -> None: offset1 = Offset2D(1, 2) offset2 = Offset2D(2, 3) offset1 -= offset2 self.assertEqual(-1, offset1.xOffset) self.assertEqual(-1, offset1.yOffset) def test_Offset_InplaceMinus_Tuple(self) -> None: offset1 = Offset2D(1, 2) offset2 = (2, 3) offset1 -= offset2 self.assertEqual(-1, offset1.xOffset) self.assertEqual(-1, offset1.yOffset) def test_Offset_InplaceMinus_Int(self) -> None: offset = Offset2D(1, 2) with self.assertRaises(TypeError): offset -= 2 pyTooling-8.11.0/tests/unit/Cartesian/Cartesian3D.py000066400000000000000000000402141513317154500222770ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ _____ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ /| _ \ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ |_ \| | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |___) | |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|____/|____/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for ... """ from math import sqrt from unittest import TestCase from pyTooling.Cartesian3D import Origin3D, Point3D, Offset3D, Size3D, LineSegment3D if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Origin(self) -> None: origin = Origin3D() self.assertIsInstance(origin.x, int) self.assertIsInstance(origin.y, int) self.assertIsInstance(origin.z, int) self.assertEqual(0, origin.x) self.assertEqual(0, origin.y) self.assertEqual(0, origin.z) self.assertTupleEqual((0, 0, 0), origin.ToTuple()) self.assertEqual("Origin3D(0, 0, 0)", repr(origin)) def test_Point_int(self) -> None: point = Point3D(1, 2, 3) self.assertIsInstance(point.x, int) self.assertIsInstance(point.y, int) self.assertIsInstance(point.z, int) self.assertEqual(1, point.x) self.assertEqual(2, point.y) self.assertEqual(3, point.z) self.assertTupleEqual((1, 2, 3), point.ToTuple()) self.assertEqual("Point3D(1, 2, 3)", repr(point)) def test_Point_float(self) -> None: point = Point3D(1.0, 2.0, 3.0) self.assertIsInstance(point.x, float) self.assertIsInstance(point.y, float) self.assertIsInstance(point.z, float) self.assertEqual(1.0, point.x) self.assertEqual(2.0, point.y) self.assertEqual(3.0, point.z) self.assertTupleEqual((1.0, 2.0, 3.0), point.ToTuple()) self.assertEqual("Point3D(1.0, 2.0, 3.0)", repr(point)) def test_Point_str1(self) -> None: with self.assertRaises(TypeError): _ =Point3D("1", 2, 3) def test_Point_str2(self) -> None: with self.assertRaises(TypeError): _ =Point3D(1, "2", 3) def test_Point_str3(self) -> None: with self.assertRaises(TypeError): _ =Point3D(1, 2, "3") def test_Offset_int(self) -> None: offset = Offset3D(1, 2, 3) self.assertIsInstance(offset.xOffset, int) self.assertIsInstance(offset.yOffset, int) self.assertIsInstance(offset.zOffset, int) self.assertEqual(1, offset.xOffset) self.assertEqual(2, offset.yOffset) self.assertEqual(3, offset.zOffset) self.assertTupleEqual((1, 2, 3), offset.ToTuple()) self.assertEqual("Offset3D(1, 2, 3)", repr(offset)) def test_Offset_float(self) -> None: offset = Offset3D(1.0, 2.0, 3.0) self.assertIsInstance(offset.xOffset, float) self.assertIsInstance(offset.yOffset, float) self.assertIsInstance(offset.zOffset, float) self.assertEqual(1.0, offset.xOffset) self.assertEqual(2.0, offset.yOffset) self.assertEqual(3.0, offset.zOffset) self.assertTupleEqual((1.0, 2.0, 3.0), offset.ToTuple()) self.assertEqual("Offset3D(1.0, 2.0, 3.0)", repr(offset)) def test_Offset_str1(self) -> None: with self.assertRaises(TypeError): _ = Offset3D("1", 2, 3) def test_Offset_str2(self) -> None: with self.assertRaises(TypeError): _ = Offset3D(1, "2", 3) def test_Offset_str3(self) -> None: with self.assertRaises(TypeError): _ = Offset3D(1, 2, "3") def test_Size_int(self) -> None: size = Size3D(1, 2, 3) self.assertIsInstance(size.width, int) self.assertIsInstance(size.height, int) self.assertIsInstance(size.depth, int) self.assertEqual(1, size.width) self.assertEqual(2, size.height) self.assertEqual(3, size.depth) self.assertTupleEqual((1, 2, 3), size.ToTuple()) self.assertEqual("Size3D(1, 2, 3)", repr(size)) def test_Size_float(self) -> None: size = Size3D(1.0, 2.0, 3.0) self.assertIsInstance(size.width, float) self.assertIsInstance(size.height, float) self.assertIsInstance(size.depth, float) self.assertEqual(1.0, size.width) self.assertEqual(2.0, size.height) self.assertEqual(3.0, size.depth) self.assertTupleEqual((1.0, 2.0, 3.0), size.ToTuple()) self.assertEqual("Size3D(1.0, 2.0, 3.0)", repr(size)) def test_Size_str1(self) -> None: with self.assertRaises(TypeError): _ = Size3D("1", 2, 3) def test_Size_str2(self) -> None: with self.assertRaises(TypeError): _ = Size3D(1, "2", 3) def test_Size_str3(self) -> None: with self.assertRaises(TypeError): _ = Size3D(1, 2, "3") def test_LineSegment(self) -> None: point1 = Point3D(1, 2, 3) point2 = Point3D(2, 3, 4) line = LineSegment3D(point1, point2) offset = Offset3D(1, 1, 1) self.assertEqual(sqrt(3), line.Length) self.assertEqual(offset, line.ToOffset()) self.assertTupleEqual(((1, 2, 3), (2, 3, 4)), line.ToTuple()) class Copy(TestCase): def test_Origin(self) -> None: origin = Origin3D() with self.assertRaises(RuntimeError): _ = origin.Copy() def test_Point_int(self) -> None: point = Point3D(1, 2, 3) newPoint = point.Copy() self.assertIsInstance(newPoint.x, int) self.assertIsInstance(newPoint.y, int) self.assertIsInstance(newPoint.z, int) self.assertEqual(1, newPoint.x) self.assertEqual(2, newPoint.y) self.assertEqual(3, newPoint.z) self.assertTupleEqual((1, 2, 3), newPoint.ToTuple()) self.assertEqual("Point3D(1, 2, 3)", repr(newPoint)) def test_Point_float(self) -> None: point = Point3D(1.0, 2.0, 3.0) newPoint = point.Copy() self.assertIsInstance(newPoint.x, float) self.assertIsInstance(newPoint.y, float) self.assertIsInstance(newPoint.z, float) self.assertEqual(1.0, newPoint.x) self.assertEqual(2.0, newPoint.y) self.assertEqual(3.0, newPoint.z) self.assertTupleEqual((1.0, 2.0, 3.0), newPoint.ToTuple()) self.assertEqual("Point3D(1.0, 2.0, 3.0)", repr(newPoint)) def test_Offset_int(self) -> None: offset = Offset3D(1, 2, 3) newOffset = offset.Copy() self.assertIsInstance(newOffset.xOffset, int) self.assertIsInstance(newOffset.yOffset, int) self.assertIsInstance(newOffset.zOffset, int) self.assertEqual(1, newOffset.xOffset) self.assertEqual(2, newOffset.yOffset) self.assertEqual(3, newOffset.zOffset) self.assertTupleEqual((1, 2, 3), newOffset.ToTuple()) self.assertEqual("Offset3D(1, 2, 3)", repr(newOffset)) def test_Offset_float(self) -> None: offset = Offset3D(1.0, 2.0, 3.0) newOffset = offset.Copy() self.assertIsInstance(newOffset.xOffset, float) self.assertIsInstance(newOffset.yOffset, float) self.assertIsInstance(newOffset.zOffset, float) self.assertEqual(1.0, newOffset.xOffset) self.assertEqual(2.0, newOffset.yOffset) self.assertEqual(3.0, newOffset.zOffset) self.assertTupleEqual((1.0, 2.0, 3.0), newOffset.ToTuple()) self.assertEqual("Offset3D(1.0, 2.0, 3.0)", repr(newOffset)) def test_Size_int(self) -> None: size = Size3D(1, 2, 3) newSize = size.Copy() self.assertIsInstance(newSize.width, int) self.assertIsInstance(newSize.height, int) self.assertIsInstance(newSize.depth, int) self.assertEqual(1, newSize.width) self.assertEqual(2, newSize.height) self.assertEqual(3, newSize.depth) self.assertTupleEqual((1, 2, 3), newSize.ToTuple()) self.assertEqual("Size3D(1, 2, 3)", repr(newSize)) def test_Size_float(self) -> None: size = Size3D(1.0, 2.0, 3.0) newSize = size.Copy() self.assertIsInstance(newSize.width, float) self.assertIsInstance(newSize.height, float) self.assertIsInstance(newSize.depth, float) self.assertEqual(1.0, newSize.width) self.assertEqual(2.0, newSize.height) self.assertEqual(3.0, newSize.depth) self.assertTupleEqual((1.0, 2.0, 3.0), newSize.ToTuple()) self.assertEqual("Size3D(1.0, 2.0, 3.0)", repr(newSize)) class Comparison(TestCase): def test_Offset_Equal_Offset(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = Offset3D(1, 2, 3) self.assertEqual(offset1, offset2) def test_Offset_Equal_Tuple(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = (1, 2, 3) self.assertEqual(offset1, offset2) def test_Offset_Equal_int(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = 2 with self.assertRaises(TypeError): _ = offset1 == offset2 def test_Offset_Unequal_Offset(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = Offset3D(2, 3, 4) self.assertTrue(offset1 != offset2) class PointArithmetic(TestCase): def test_Point_Plus_Point(self) -> None: point1 = Point3D(1, 2, 3) point2 = Point3D(2, 3, 4) with self.assertRaises(TypeError): _ = point1 + point2 def test_Point_Plus_Offset(self) -> None: point = Point3D(1, 2, 3) offset = Offset3D(2, 3, 4) newPoint = point + offset self.assertEqual(3, newPoint.x) self.assertEqual(5, newPoint.y) self.assertEqual(7, newPoint.z) def test_Point_Plus_Tuple(self) -> None: point = Point3D(1, 2, 3) offset = (2, 3, 4) newPoint = point + offset self.assertEqual(3, newPoint.x) self.assertEqual(5, newPoint.y) self.assertEqual(7, newPoint.z) def test_Point_InplacePlus_Offset(self) -> None: point = Point3D(1, 2, 3) offset = Offset3D(2, 3, 4) point += offset self.assertEqual(3, point.x) self.assertEqual(5, point.y) self.assertEqual(7, point.z) def test_Point_InplacePlus_Tuple(self) -> None: point = Point3D(1, 2, 3) offset = (2, 3, 4) point += offset self.assertEqual(3, point.x) self.assertEqual(5, point.y) self.assertEqual(7, point.z) def test_Point_InplacePlus_Int(self) -> None: point = Point3D(1, 2, 3) with self.assertRaises(TypeError): point += 2 def test_Point_Minus_Offset(self) -> None: point = Point3D(1, 2, 3) offset = Offset3D(2, 3, 4) newPoint = point + -offset self.assertEqual(-1, newPoint.x) self.assertEqual(-1, newPoint.y) self.assertEqual(-1, newPoint.z) def test_Point_Minus_Point(self) -> None: point1 = Point3D(1, 2, 3) point2 = Point3D(2, 3, 4) offset = point2 - point1 self.assertEqual(1, offset.xOffset) self.assertEqual(1, offset.yOffset) self.assertEqual(1, offset.zOffset) def test_Point_Minus_Tuple(self) -> None: point = Point3D(1, 2, 3) with self.assertRaises(TypeError): _ = point - 2 def test_Point_InplaceMinus_Offset(self) -> None: point = Point3D(1, 2, 3) offset = Offset3D(2, 3, 4) point -= offset self.assertEqual(-1, point.x) self.assertEqual(-1, point.y) self.assertEqual(-1, point.z) def test_Point_InplaceMinus_Tuple(self) -> None: point = Point3D(1, 2, 3) offset = (2, 3, 4) point -= offset self.assertEqual(-1, point.x) self.assertEqual(-1, point.y) self.assertEqual(-1, point.z) def test_Point_InplaceMinus_Int(self) -> None: point = Point3D(1, 2, 3) with self.assertRaises(TypeError): point -= 2 class OffsetArithmetic(TestCase): def test_Offset_Plus_Offset(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = Offset3D(2, 3, 4) newOffset = offset1 + offset2 self.assertEqual(3, newOffset.xOffset) self.assertEqual(5, newOffset.yOffset) self.assertEqual(7, newOffset.zOffset) def test_Offset_Plus_Tuple(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = (2, 3, 4) newOffset = offset1 + offset2 self.assertEqual(3, newOffset.xOffset) self.assertEqual(5, newOffset.yOffset) self.assertEqual(7, newOffset.zOffset) def test_Offset_Plus_int(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = 2 with self.assertRaises(TypeError): _ = offset1 + offset2 def test_Offset_InplacePlus_Offset(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = Offset3D(2, 3, 4) offset1 += offset2 self.assertEqual(3, offset1.xOffset) self.assertEqual(5, offset1.yOffset) self.assertEqual(7, offset1.zOffset) def test_Offset_InplacePlus_Tuple(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = (2, 3, 4) offset1 += offset2 self.assertEqual(3, offset1.xOffset) self.assertEqual(5, offset1.yOffset) self.assertEqual(7, offset1.zOffset) def test_Offset_InplacePlus_Int(self) -> None: offset = Offset3D(1, 2, 3) with self.assertRaises(TypeError): offset += 2 def test_Offset_Minus_Offset(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = Offset3D(2, 3, 4) newOffset = offset1 - offset2 self.assertEqual(-1, newOffset.xOffset) self.assertEqual(-1, newOffset.yOffset) self.assertEqual(-1, newOffset.zOffset) def test_Offset_Minus_Tuple(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = (2, 3, 4) newOffset = offset1 - offset2 self.assertEqual(-1, newOffset.xOffset) self.assertEqual(-1, newOffset.yOffset) self.assertEqual(-1, newOffset.zOffset) def test_Offset_Minus_int(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = 2 with self.assertRaises(TypeError): _ = offset1 - offset2 def test_Offset_InplaceMinus_Offset(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = Offset3D(2, 3, 4) offset1 -= offset2 self.assertEqual(-1, offset1.xOffset) self.assertEqual(-1, offset1.yOffset) self.assertEqual(-1, offset1.zOffset) def test_Offset_InplaceMinus_Tuple(self) -> None: offset1 = Offset3D(1, 2, 3) offset2 = (2, 3, 4) offset1 -= offset2 self.assertEqual(-1, offset1.xOffset) self.assertEqual(-1, offset1.yOffset) self.assertEqual(-1, offset1.zOffset) def test_Offset_InplaceMinus_Int(self) -> None: offset = Offset3D(1, 2, 3) with self.assertRaises(TypeError): offset -= 2 pyTooling-8.11.0/tests/unit/Cartesian/Shapes.py000066400000000000000000000115461513317154500214300ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ ____ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ \| _ \ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ __) | | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |/ __/| |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|_____|____/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for ... """ from unittest import TestCase from pyTooling.Cartesian2D import Point2D, LineSegment2D from pyTooling.Cartesian2D.Shapes import Trapezium, Rectangle, Square if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Trapezium(self) -> None: point00 = Point2D(1, 1) point01 = Point2D(3, 1) point11 = Point2D(3, 3) point10 = Point2D(1, 3) trapezium = Trapezium(point00, point01, point11, point10) def test_Trapezium_str1(self) -> None: point01 = Point2D(3, 1) point11 = Point2D(3, 3) point10 = Point2D(1, 3) with self.assertRaises(TypeError): _ = Trapezium("1, 1", point01, point11, point10) def test_Trapezium_str2(self) -> None: point00 = Point2D(1, 1) point11 = Point2D(3, 3) point10 = Point2D(1, 3) with self.assertRaises(TypeError): _ = Trapezium(point00, "3, 1", point11, point10) def test_Trapezium_str3(self) -> None: point00 = Point2D(1, 1) point01 = Point2D(3, 1) point10 = Point2D(1, 3) with self.assertRaises(TypeError): _ = Trapezium(point00, point01, "3, 3", point10) def test_Trapezium_str4(self) -> None: point00 = Point2D(1, 1) point01 = Point2D(3, 1) point11 = Point2D(3, 3) with self.assertRaises(TypeError): _ = Trapezium(point00, point01, point11, "1, 3") pyTooling-8.11.0/tests/unit/Cartesian/Volumes.py000066400000000000000000000075651513317154500216450ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ _____ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ /| _ \ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ |_ \| | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |___) | |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|____/|____/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for ... """ from unittest import TestCase from pyTooling.Cartesian3D import Point3D from pyTooling.Cartesian3D.Volumes import Volume, Cuboid, Cube if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Cube(self) -> None: origin = Cube() pyTooling-8.11.0/tests/unit/Common/000077500000000000000000000000001513317154500171435ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Common/ContextManager.py000066400000000000000000000112341513317154500224350ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ ___ _ __ ___ ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | | | | | | | | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_| |_|_| |_| |_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for :class:`~pyTooling.Common.ChangeDirectory`. """ from pathlib import Path from unittest import TestCase from pyTooling.Common import ChangeDirectory as ChangeDir if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class ChangeDirectory(TestCase): def test_ChangeDirectory(self) -> None: before = Path.cwd() path = Path("tests/unit/Common") with ChangeDir(path) as p: self.assertEqual(before / path, Path.cwd()) self.assertEqual(before / path, p) self.assertEqual(before, Path.cwd()) def test_DoubleChangeDirectory(self) -> None: before = Path.cwd() outerPath = Path("tests/unit/Common") innerPath = Path("../Attributes") with ChangeDir(outerPath) as op: self.assertEqual((before / outerPath).resolve(), Path.cwd()) self.assertEqual((before / outerPath).resolve(), op) with ChangeDir(innerPath) as ip: self.assertEqual((before / outerPath / innerPath).resolve(), Path.cwd()) self.assertEqual((before / outerPath / innerPath).resolve(), ip) self.assertEqual((before / outerPath).resolve(), Path.cwd()) self.assertEqual(before, Path.cwd()) pyTooling-8.11.0/tests/unit/Common/Dictionary.py000066400000000000000000000162441513317154500216310ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ ___ _ __ ___ ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | | | | | | | | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_| |_|_| |_| |_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for :func:`firstKey`, :func:`firstValue`, :func:`firstPair`, :func:`mergedicts` and :func:`zipdicts`. """ from unittest import TestCase from pytest import mark from pyTooling.Common import firstKey, firstValue, firstPair, mergedicts, zipdicts from pyTooling.Platform import CurrentPlatform if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class First(TestCase): def test_FirstKey0(self) -> None: d = {} with self.assertRaises(ValueError): _ = firstKey(d) def test_FirstKey1(self) -> None: d = {"1": 1} f = firstKey(d) self.assertEqual("1", f) def test_FirstKey2(self) -> None: d = {"1": 1, "2": 2} f = firstKey(d) self.assertEqual("1", f) def test_FirstValue0(self) -> None: d = {} with self.assertRaises(ValueError): _ = firstValue(d) def test_FirstValue1(self) -> None: d = {"1": 1} f = firstValue(d) self.assertEqual(1, f) def test_FirstValue2(self) -> None: d = {"1": 1, "2": 2} f = firstValue(d) self.assertEqual(1, f) def test_FirstPair0(self) -> None: d = {} with self.assertRaises(ValueError): _ = firstPair(d) def test_FirstPair1(self) -> None: d = {"1": 1} f = firstPair(d) self.assertTupleEqual(("1", 1), f) def test_FirstPair2(self) -> None: d = {"1": 1, "2": 2} f = firstPair(d) self.assertTupleEqual(("1", 1), f) class Merge(TestCase): def test_NoDicts(self) -> None: with self.assertRaises(ValueError): _ = mergedicts() with self.assertRaises(ValueError): _ = mergedicts(filter=lambda k, v: True) def test_Merge1(self) -> None: d1 = {"1": 1, "2": 2} expected = ( ("1", 1), ("2", 2) ) m = mergedicts(d1) self.assertTupleEqual(expected, tuple(m.items())) def test_Merge2(self) -> None: d1 = {"1": 1, "2": 2} d2 = {"3": 3, "4": 4} expected = ( ("1", 1), ("2", 2), ("3", 3), ("4", 4) ) m = mergedicts(d1, d2) self.assertTupleEqual(expected, tuple(m.items())) def test_Merge3(self) -> None: d1 = {"1": 1, "2": 2} d2 = {"3": 3, "4": 4} d3 = {"5": 5, "6": 6} expected = ( ("1", 1), ("2", 2), ("3", 3), ("4", 4), ("5", 5), ("6", 6) ) m = mergedicts(d1, d2, d3) self.assertTupleEqual(expected, tuple(m.items())) def test_Merge2Filter(self) -> None: d1 = {"1": 1, "2": 2} d2 = {"3": 3, "4": 4} expected = ( ("2", 2), ("4", 4) ) m = mergedicts(d1, d2, filter=lambda key, value: value % 2 == 0) self.assertTupleEqual(expected, tuple(m.items())) class Zip(TestCase): def test_NoDicts(self) -> None: with self.assertRaises(ValueError): _ = zipdicts() @mark.skipif(CurrentPlatform.IsPyPy and CurrentPlatform.PythonVersion == "3.10", reason="Tuple/list expansion with *foo is broken in pypy-3.10.") def test_Zip1_1(self) -> None: d1 = {"a": 1} d2 = {"b": "2"} with self.assertRaises(KeyError): for key, valueA, valueB in zipdicts(d1, d2): pass def test_Zip1_2(self) -> None: d1 = {"a": 1} d2 = {"a": "1", "b": "2"} with self.assertRaises(ValueError): _ = zipdicts(d1, d2) def test_Zip2_1(self) -> None: d1 = {"a": 1, "b": 2} d2 = {"a": "1"} with self.assertRaises(ValueError): _ = zipdicts(d1, d2) def test_Zip2_2(self) -> None: d1 = {"a": 1, "b": 2} d2 = {"a": "1", "b": "2"} expected = ( ("a", 1, "1"), ("b", 2, "2"), ) z = zipdicts(d1, d2) self.assertTupleEqual(expected, tuple(z)) def test_Iterate(self) -> None: d1 = {"a": 1, "b": 2} d2 = {"a": "1", "b": "2"} expected = ( ("a", 1, "1"), ("b", 2, "2"), ) i = 0 for key, value1, value2 in zipdicts(d1, d2): self.assertTupleEqual(expected[i], (key, value1, value2)) i += 1 pyTooling-8.11.0/tests/unit/Common/IsNestedClass.py000066400000000000000000000106651513317154500222310ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ ___ _ __ ___ ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | | | | | | | | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_| |_|_| |_| |_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for :func:`isnestedclass`. """ from unittest import TestCase from pyTooling.Common import isnestedclass if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Class_1: class Class_11: class Class_111: pass class Class_1_1(Class_1): pass class Class_2: pass class IsNestedClass(TestCase): def test_SameClass(self) -> None: self.assertFalse(isnestedclass(Class_1, Class_1)) def test_NestedClass(self) -> None: self.assertTrue(isnestedclass(Class_1.Class_11, Class_1)) def test_DerivedClass(self) -> None: self.assertTrue(isnestedclass(Class_1_1.Class_11, Class_1)) self.assertTrue(isnestedclass(Class_1_1.Class_11, Class_1_1)) def test_DoubleNestedClass(self) -> None: self.assertFalse(isnestedclass(Class_1.Class_11.Class_111, Class_1)) def test_ParallelClass(self) -> None: self.assertFalse(isnestedclass(Class_2, Class_1)) pyTooling-8.11.0/tests/unit/Common/Iterable.py000066400000000000000000000114461513317154500212520ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ ___ _ __ ___ ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | | | | | | | | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_| |_|_| |_| |_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for :func:`firstItem` and :func:`lastItem`. """ from unittest import TestCase from pyTooling.Common import firstItem, lastItem, count if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Count(TestCase): def test_count_empty(self) -> None: c = count(range(0)) self.assertEqual(0, c) def test_count_1(self) -> None: c = count(range(1)) self.assertEqual(1, c) def test_count_5(self) -> None: c = count(range(5)) self.assertEqual(5, c) def test_count_10(self) -> None: length = 10 l = [i for i in range(length)] g = (i for i in l) c = count(g) self.assertEqual(length, c) class First(TestCase): def test_FirstItem0(self) -> None: d = [] with self.assertRaises(ValueError): _ = firstItem(d) def test_FirstItem1(self) -> None: d = [1] f = firstItem(d) self.assertEqual(1, f) def test_FirstItem2(self) -> None: d = [1, 2] f = firstItem(d) self.assertEqual(1, f) class Last(TestCase): def test_LastItem0(self) -> None: d = [] with self.assertRaises(ValueError): _ = lastItem(d) def test_LastItem1(self) -> None: d = [1] l = lastItem(d) self.assertEqual(1, l) def test_LastItem2(self) -> None: d = [1, 2] l = lastItem(d) self.assertEqual(2, l) pyTooling-8.11.0/tests/unit/Common/SizeOf.py000066400000000000000000000111111513317154500207070ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ ___ _ __ ___ ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | | | | | | | | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_| |_|_| |_| |_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for :func:`isnestedclass`. """ from unittest import TestCase from pytest import mark from pyTooling.Common import getsizeof from pyTooling.Platform import CurrentPlatform if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class ObjectSizes(TestCase): @mark.skipif(CurrentPlatform.IsPyPy, reason="getsizeof: not supported on PyPy") def test_EmptyClass(self) -> None: class C: pass c = C() self.assertLessEqual(getsizeof(c), 360) @mark.skipif(CurrentPlatform.IsPyPy, reason="getsizeof: not supported on PyPy") def test_ClassWith2DictMembers(self) -> None: class C: def __init__(self) -> None: self._a = 1 self._b = 2 c = C() self.assertLessEqual(getsizeof(c), 520) @mark.skipif(CurrentPlatform.IsPyPy, reason="getsizeof: not supported on PyPy") def test_ClassWith2SlotMembers(self) -> None: class C: __slots__ = ("_a", "_b") def __init__(self) -> None: self._a = 1 self._b = 2 c = C() self.assertLessEqual(getsizeof(c), 128) pyTooling-8.11.0/tests/unit/Common/Stopwatch.py000066400000000000000000000264051513317154500215000ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ ___ _ __ ___ ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | | | | | | | | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_| |_|_| |_| |_|\___/|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for TBD.""" from time import sleep from unittest import TestCase from pyTooling.Exceptions import ToolingException from pyTooling.Platform import CurrentPlatform from pyTooling.Stopwatch import Stopwatch, StopwatchException if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Operations(TestCase): DELAY = 0.5 PAUSE = 0.9 INACCURACY = 2.7 if CurrentPlatform.IsNativeMacOS else 1.25 def test_StartStart(self) -> None: sw = Stopwatch() sw.Start() with self.assertRaises(ToolingException): sw.Start() def test_Split(self) -> None: sw = Stopwatch() with self.assertRaises(ToolingException): sw.Split() def test_Pause(self) -> None: sw = Stopwatch() with self.assertRaises(ToolingException): sw.Pause() def test_Resume(self) -> None: sw = Stopwatch() with self.assertRaises(ToolingException): sw.Resume() def test_Stop(self) -> None: sw = Stopwatch() with self.assertRaises(ToolingException): sw.Stop() def test_StartStop(self) -> None: print() sw = Stopwatch() sw.Start() sleep(self.DELAY) # 500 ms diff = sw.Stop() print(f"Duration for 'sleep({self.DELAY:0.3f})': {diff:0.6f} us") self.assertLessEqual(diff, self.DELAY * self.INACCURACY) self.assertFalse(sw.HasSplitTimes) self.assertFalse(sw.IsStarted) self.assertFalse(sw.IsPaused) self.assertFalse(sw.IsRunning) self.assertTrue(sw.IsStopped) self.assertEqual(0, sw.SplitCount) self.assertEqual(0, sw.ActiveCount) self.assertEqual(0, sw.InactiveCount) self.assertEqual(0, len(sw)) def test_StartPauseStop(self) -> None: print() sw = Stopwatch() sw.Start() sleep(self.DELAY) # 500 ms diff1 = sw.Pause() sleep(self.PAUSE) # 200 ms diff2 = sw.Stop() total = sw.Duration print(f"Duration for '1st sleep({self.DELAY:0.3f})': {diff1:0.6f} us") self.assertLessEqual(diff1, self.DELAY * self.INACCURACY) print(f"Duration for '1st pause({self.PAUSE:0.3f})': {diff2:0.6f} us") self.assertLessEqual(diff2, self.PAUSE * self.INACCURACY) print(f"Duration for '1x sleep({self.DELAY:0.3f}) + 1x pause({self.PAUSE:0.3f})': {total:0.6f} us") self.assertLessEqual(total, (1 * self.DELAY + 1 * self.PAUSE) * self.INACCURACY) self.assertTrue(sw.HasSplitTimes) self.assertEqual(2, sw.SplitCount) self.assertEqual(1, sw.ActiveCount) self.assertEqual(1, sw.InactiveCount) self.assertEqual(2, len(sw)) def test_StartPauseResumeStop(self) -> None: print() sw = Stopwatch() sw.Start() sleep(self.DELAY) # 500 ms diff1 = sw.Pause() sleep(self.PAUSE) # 200 ms diff2 = sw.Resume() sleep(self.DELAY) # 500 ms diff3 = sw.Stop() total = sw.Duration print(f"Duration for '1st sleep({self.DELAY:0.3f})': {diff1:0.6f} us") self.assertLessEqual(diff1, self.DELAY * self.INACCURACY) print(f"Duration for '1st pause({self.PAUSE:0.3f})': {diff2:0.6f} us") self.assertLessEqual(diff2, self.PAUSE * self.INACCURACY) print(f"Duration for '2nd sleep({self.DELAY:0.3f})': {diff3:0.6f} us") self.assertLessEqual(diff3, self.DELAY * self.INACCURACY) print(f"Duration for '2x sleep({self.DELAY:0.3f}) + 1x pause({self.PAUSE:0.3f})': {total:0.6f} us") self.assertLessEqual(total, (2 * self.DELAY + 1 * self.PAUSE) * self.INACCURACY) seq = ((diff1, True), (diff2, False), (diff3, True)) self.assertTrue(sw.HasSplitTimes) self.assertEqual(3, sw.SplitCount) self.assertEqual(2, sw.ActiveCount) self.assertEqual(1, sw.InactiveCount) self.assertEqual(3, len(sw)) self.assertTupleEqual(seq[0], sw[0]) self.assertTupleEqual(seq[1], sw[1]) self.assertTupleEqual(seq[2], sw[2]) self.assertTupleEqual(seq, tuple(t for t in sw)) class Formatting(TestCase): def test_NoName(self) -> None: print() sw = Stopwatch() result = str(sw) print(result) self.assertEqual("Stopwatch: not started", result) def test_WithName(self) -> None: print() sw = Stopwatch("foo") result = str(sw) print(result) self.assertEqual("Stopwatch foo: not started", result) def test_WithName_Running(self) -> None: print() sw = Stopwatch("foo") sw.Start() result = str(sw) sw.Stop() print(result) self.assertRegex(result, r"Stopwatch foo \(running\): ") def test_WithName_Paused(self) -> None: print() sw = Stopwatch("foo") sw.Start() sw.Pause() result = str(sw) sw.Stop() print(result) self.assertRegex(result, r"Stopwatch foo \(paused\): ") def test_WithName_Resumed(self) -> None: print() sw = Stopwatch("foo") sw.Start() sw.Pause() sw.Resume() result = str(sw) sw.Stop() print(result) self.assertRegex(result, r"Stopwatch foo \(running\): ") def test_WithName_Stopped(self) -> None: print() sw = Stopwatch("foo") sw.Start() sw.Stop() result = str(sw) print(result) self.assertRegex(result, r"Stopwatch foo \(stopped\): ") class ContextManagerProtocol(TestCase): DELAY = 0.5 PAUSE = 0.9 INACCURACY = 2.7 if CurrentPlatform.IsNativeMacOS else 1.25 def test_OneLiner(self) -> None: print() with Stopwatch() as sw: sleep(self.DELAY) # 500 ms print(f"Duration for '1st sleep({self.DELAY:0.3f})': {sw.Duration:0.6f} us") self.assertLessEqual(sw.Duration, self.DELAY * self.INACCURACY) def test_PreCreated(self) -> None: print() sw = Stopwatch() with sw: sleep(self.DELAY) # 500 ms print(f"Duration for '1st sleep({self.DELAY:0.3f})': {sw.Duration:0.6f} us") self.assertLessEqual(sw.Duration, self.DELAY * self.INACCURACY) def test_ReuseContext_StartStop(self) -> None: print() sw = Stopwatch() with sw: sleep(self.DELAY) # 500 ms print(f"Duration for '1st sleep({self.DELAY:0.3f})': {sw.Duration:0.6f} us") self.assertEqual(1, sw.ActiveCount) self.assertLessEqual(sw.Activity, self.DELAY * self.INACCURACY) self.assertLessEqual(sw.Duration, self.DELAY * self.INACCURACY) with self.assertRaises(StopwatchException): with sw: sleep(self.DELAY) # 500 ms def test_ReuseContext_ResumePause(self) -> None: print() sw = Stopwatch(preferPause=True) with sw: sleep(self.DELAY) # 500 ms print(f"Duration for '1st sleep({self.DELAY:0.3f})': {sw.Duration:0.6f} us") self.assertEqual(1, sw.ActiveCount) self.assertEqual(0, sw.InactiveCount) self.assertLessEqual(sw.Activity, self.DELAY * self.INACCURACY) self.assertLessEqual(sw.Duration, self.DELAY * self.INACCURACY) with sw: sleep(self.DELAY) # 500 ms print(f"Duration for '2st sleep({self.DELAY:0.3f})': {sw.Duration:0.6f} us") self.assertEqual(3, len(sw)) self.assertEqual(2, sw.ActiveCount) self.assertEqual(1, sw.InactiveCount) self.assertLessEqual(sw.Activity, 2 * self.DELAY * self.INACCURACY) def test_ReuseContext_Loop(self) -> None: print() sw = Stopwatch(preferPause=True) for i in range(5): with sw: sleep(self.DELAY / 5) # 100 ms sleep(self.PAUSE / 2) # 450 ms sw.Stop() print(f"Start/Stop/Diff: {sw.StartTime}/{sw.StopTime}/{sw.StopTime - sw.StartTime}/{sw.Duration}") print(f"Activity/Inactivity: {sw.Activity}/{sw.Inactivity}") print("Iterator: __iter__") for duration, activity in sw: print(f" {duration} {'running' if activity else 'paused'}") self.assertEqual(5, sw.ActiveCount) self.assertEqual(5, sw.InactiveCount) def test_Splits(self) -> None: print() with Stopwatch() as sw: sleep(self.DELAY) # 500 ms sw.Split() sleep(self.DELAY) # 500 ms sw.Split() sleep(self.DELAY) # 500 ms print(f"Start/Stop/Diff: {sw.StartTime}/{sw.StopTime}/{sw.StopTime - sw.StartTime}/{sw.Duration}") print(f"Activity/Inactivity: {sw.Activity}/{sw.Inactivity}") print("Iterator: __iter__") for duration, activity in sw: print(f" {duration} {'running' if activity else 'paused'}") self.assertAlmostEqual(sw.Duration, sw.Activity) self.assertEqual(0, sw.Inactivity) self.assertEqual(0, sw.InactiveCount) pyTooling-8.11.0/tests/unit/Configuration/000077500000000000000000000000001513317154500205225ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Configuration/JSON.py000066400000000000000000000143171513317154500216530ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ __ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for JSON based configurations.""" from pathlib import Path from unittest import TestCase from pyTooling.Configuration.JSON import Configuration class ReadingValues(TestCase): def test_SimpleString(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.json")) self.assertEqual("string_1", config["value_1"]) node_1 = config["node_1"] self.assertEqual("string_11", node_1["value_11"]) self.assertEqual("string_12", config["node_1"]["value_12"]) def test_Root(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.json")) self.assertEqual(4, len(config)) self.assertTrue("Install" in config) def test_Dictionary(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.json")) node_1 = config["node_1"] self.assertEqual(2, len(node_1)) iterator = iter(node_1) first = next(iterator) self.assertEqual("string_11", first) second = next(iterator) self.assertEqual("string_12", second) def test_Sequence(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.json")) node_2 = config["node_2"] self.assertEqual(2, len(node_2)) iterator = iter(node_2) first = next(iterator) self.assertEqual("string_2111", node_2[0]["list_211"]["key_2111"]) self.assertEqual("string_2111", first["list_211"]["key_2111"]) second = next(iterator) self.assertEqual("string_2211", node_2[1]["list_221"]["key_2211"]) self.assertEqual("string_2211", second["list_221"]["key_2211"]) with self.assertRaises(StopIteration): _ = next(iterator) def test_PathExpressionToNode(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.json")) node = config.QueryPath("Install:VendorA:ToolA:2020") self.assertEqual(r"C:\VendorA\ToolA\2020", node["InstallDir"]) def test_PathExpressionToValue(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.json")) value = config.QueryPath("Install:VendorA:ToolA:2020:InstallDir") self.assertEqual(r"C:\VendorA\ToolA\2020", value) def test_Variables(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.json")) self.assertEqual(r"C:\VendorA\ToolA\2020", config["Install"]["VendorA"]["ToolA"]["2020"]["InstallDir"]) self.assertEqual(r"C:\VendorA\Tool_A\2021.10", config["Install"]["VendorA"]["ToolA"]["2021.10"]["InstallDir"]) self.assertEqual(r"C:\VendorA\ToolA\2020\bin", config["Install"]["VendorA"]["ToolA"]["2020"]["BinaryDir"]) def test_NestedVariables(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.json")) self.assertEqual(r"C:\VendorA\ToolA\2020", config["Install"]["VendorA"]["ToolA"]["Defaults"]["InstallDir"]) self.assertEqual(r"C:\VendorA\ToolA\2020\bin", config["Install"]["VendorA"]["ToolA"]["Defaults"]["BinaryDir"]) pyTooling-8.11.0/tests/unit/Configuration/YAML.py000066400000000000000000000143071513317154500216430ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ __ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for YAML based configurations.""" from pathlib import Path from unittest import TestCase from pyTooling.Configuration.YAML import Configuration class ReadingValues(TestCase): def test_SimpleString(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.yml")) self.assertEqual("string_1", config["value_1"]) node_1 = config["node_1"] self.assertEqual("string_11", node_1["value_11"]) self.assertEqual("string_12", config["node_1"]["value_12"]) def test_Root(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.yml")) self.assertEqual(4, len(config)) self.assertTrue("Install" in config) def test_Dictionary(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.yml")) node_1 = config["node_1"] self.assertEqual(2, len(node_1)) iterator = iter(node_1) first = next(iterator) self.assertEqual("string_11", first) second = next(iterator) self.assertEqual("string_12", second) def test_Sequence(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.yml")) node_2 = config["node_2"] self.assertEqual(2, len(node_2)) iterator = iter(node_2) first = next(iterator) self.assertEqual("string_2111", node_2[0]["list_211"]["key_2111"]) self.assertEqual("string_2111", first["list_211"]["key_2111"]) second = next(iterator) self.assertEqual("string_2211", node_2[1]["list_221"]["key_2211"]) self.assertEqual("string_2211", second["list_221"]["key_2211"]) with self.assertRaises(StopIteration): _ = next(iterator) def test_PathExpressionToNode(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.yml")) node = config.QueryPath("Install:VendorA:ToolA:2020") self.assertEqual(r"C:\VendorA\ToolA\2020", node["InstallDir"]) def test_PathExpressionToValue(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.yml")) value = config.QueryPath("Install:VendorA:ToolA:2020:InstallDir") self.assertEqual(r"C:\VendorA\ToolA\2020", value) def test_Variables(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.yml")) self.assertEqual(r"C:\VendorA\ToolA\2020", config["Install"]["VendorA"]["ToolA"]["2020"]["InstallDir"]) self.assertEqual(r"C:\VendorA\Tool_A\2021.10", config["Install"]["VendorA"]["ToolA"]["2021.10"]["InstallDir"]) self.assertEqual(r"C:\VendorA\ToolA\2020\bin", config["Install"]["VendorA"]["ToolA"]["2020"]["BinaryDir"]) def test_NestedVariables(self) -> None: config = Configuration(Path("tests/unit/Configuration/config.yml")) self.assertEqual(r"C:\VendorA\ToolA\2020", config["Install"]["VendorA"]["ToolA"]["Defaults"]["InstallDir"]) self.assertEqual(r"C:\VendorA\ToolA\2020\bin", config["Install"]["VendorA"]["ToolA"]["Defaults"]["BinaryDir"]) pyTooling-8.11.0/tests/unit/Configuration/__init__.py000066400000000000000000000067351513317154500226460ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ __ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for configurations.""" pyTooling-8.11.0/tests/unit/Configuration/config.json000066400000000000000000000016521513317154500226660ustar00rootroot00000000000000{ "value_1": "string_1", "node_1": { "value_11": "string_11", "value_12": "string_12" }, "node_2": [ { "list_211": { "key_2111": "string_2111", "key_2112": "string_2112" }, "list_212": "string_212" }, { "list_221": { "key_2211": "string_2211", "key_2212": "string_2212" } } ], "Install": { "VendorA": { "InstallDir": "C:\\VendorA", "ToolA": { "InstallDir": "${..:InstallDir}\\ToolA", "Defaults": { "Version": 2020, "InstallDir": "${..:${Version}:InstallDir}", "BinaryDir": "${..:${Version}:BinaryDir}" }, "2020": { "Version": 2020, "InstallDir": "${..:InstallDir}\\${Version}", "BinaryDir": "${InstallDir}\\bin" }, "2021.10": { "Version": "2021.10", "InstallDir": "${..:..:InstallDir}\\Tool_A\\${Version}", "BinaryDir": "${InstallDir}\\bin" } } }, "VendorB": { "InstallDir": "C:\\VendorB" } } } pyTooling-8.11.0/tests/unit/Configuration/config.yml000066400000000000000000000014521513317154500225140ustar00rootroot00000000000000value_1: string_1 node_1: value_11: string_11 value_12: string_12 node_2: - list_211: key_2111: string_2111 key_2112: string_2112 list_212: string_212 - list_221: key_2211: string_2211 key_2212: string_2212 Install: VendorA: InstallDir: 'C:\VendorA' ToolA: InstallDir: '${..:InstallDir}\ToolA' Defaults: Version: 2020 InstallDir: '${..:${Version}:InstallDir}' BinaryDir: '${..:${Version}:BinaryDir}' 2020: Version: 2020 InstallDir: '${..:InstallDir}\${Version}' BinaryDir: '${InstallDir}\bin' '2021.10': Version: '2021.10' InstallDir: '${..:..:InstallDir}\Tool_A\${Version}' BinaryDir: '${InstallDir}\bin' VendorB: InstallDir: 'C:\VendorB' pyTooling-8.11.0/tests/unit/Decorators/000077500000000000000000000000001513317154500200205ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Decorators/Decorators.py000066400000000000000000000162761513317154500225130ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ ___ ___ _ __ __ _| |_ ___ _ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \/ __/ _ \| '__/ _` | __/ _ \| '__/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ (_| (_) | | | (_| | || (_) | | \__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___|\___\___/|_| \__,_|\__\___/|_| |___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for Decorators.""" from unittest import TestCase from pytest import mark from pyTooling.Decorators import export, InheritDocString, readonly if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) __all__ = [] @export class ExportedClass: pass class NotYetExportedClass: pass class NotExportedClass: pass @export def ExportedFunction(): pass def NotYetExportedFunction(): pass def NotExportedFunction(): pass L = lambda x: x class Export(TestCase): def test_ExportedClass(self) -> None: self.assertIn(ExportedClass.__name__, __all__) self.assertNotIn(NotExportedClass.__name__, __all__) def test_ExportedFunction(self) -> None: self.assertIn(ExportedFunction.__name__, __all__) self.assertNotIn(NotExportedFunction.__name__, __all__) def test_ExportTopLevelClass(self) -> None: export(NotYetExportedClass) def test_ExportTopLevelFunction(self) -> None: export(NotYetExportedFunction) def test_ExportTopLevelLambda(self) -> None: with self.assertRaises(TypeError): export(L) def test_ExportLocalFunction(self) -> None: with self.assertRaises(TypeError): @export def F(): pass def test_ExportLocalClass(self) -> None: with self.assertRaises(TypeError): @export class C: pass class ReadOnly(TestCase): def test_ReadOnly(self) -> None: class Data: _data: int def __init__(self, data: int) -> None: self._data = data @readonly def length(self) -> int: return 2 ** self._data d = Data(2) self.assertEqual(4, d.length) with self.assertRaises(AttributeError): d.length = 5 with self.assertRaises(AttributeError): del d.length # FIXME: needs to be activated and tested @mark.skip("EXPECTED ERROR IS NOT RAISED") def test_Setter(self) -> None: with self.assertRaises(AttributeError): class Data: _data: int def __init__(self, data: int) -> None: self._data = data @readonly def length(self) -> int: return 2 ** self._data @length.setter def length(self, value): self._data = value d = Data(6) d.length = 16 # FIXME: needs to be activated and tested @mark.skip("EXPECTED ERROR IS NOT RAISED") def test_Deleter(self) -> None: with self.assertRaises(AttributeError): class Data: _data: int def __init__(self, data: int) -> None: self._data = data @readonly def length(self) -> int: return 2 ** self._data @length.deleter def length(self, value): del self._data d = Data(7) del d.length class InheritDocStrings(TestCase): def test_Class_Copy(self) -> None: class Class1: """Class1""" @InheritDocString(Class1) class Class2(Class1): pass self.assertEqual("Class1", Class1.__doc__) self.assertEqual(Class1.__doc__, Class2.__doc__) def test_Class_Override(self) -> None: class Class1: """Class1""" @InheritDocString(Class1) class Class2(Class1): """Class2""" self.assertEqual("Class1", Class2.__doc__) def test_Class_Fallback(self) -> None: class Class1: pass @InheritDocString(Class1, merge=True) class Class2(Class1): """Class2""" self.assertIsNone(Class1.__doc__) self.assertEqual("Class2", Class2.__doc__) def test_Class_Merge(self) -> None: class Class1: """Class1""" @InheritDocString(Class1, merge=True) class Class2(Class1): """Class2""" self.assertEqual("Class1", Class1.__doc__) self.assertEqual("Class1\n\nClass2", Class2.__doc__) def test_Method(self) -> None: class Class1: def method(self): """Method's doc-string.""" class Class2(Class1): @InheritDocString(Class1) def method(self): pass self.assertEqual(Class1.method.__doc__, Class2.method.__doc__) pyTooling-8.11.0/tests/unit/Dependency/000077500000000000000000000000001513317154500177715ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Dependency/Python.py000066400000000000000000000156041513317154500216320ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \ '_ \ / _ \ '_ \ / _` |/ _ \ '_ \ / __| | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, | # # |_| |___/ |___/ |_| |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for :mod:`pyTooling.Dependency`.""" from datetime import datetime from unittest import TestCase from pytest import mark from pyTooling.Dependency.Python import PythonPackageDependencyGraph, PythonPackageIndex, Project, Release, LazyLoaderState from pyTooling.Versioning import PythonVersion if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Graph(self) -> None: graph = PythonPackageDependencyGraph("graph") def test_Index(self) -> None: graph = PythonPackageDependencyGraph("graph") index = PythonPackageIndex("index", "https://index.org/", "https://api.index.org/v4/", graph=graph) self.assertEqual("https://index.org/", str(index.URL)) self.assertEqual("https://api.index.org/v4/", str(index.API)) @mark.xfail(reason="LazyLoader algorithm conflicts with manually initialized fields.") def test_Project(self) -> None: graph = PythonPackageDependencyGraph("graph") index = PythonPackageIndex("index", "https://index.org/", "https://api.index.org/v4/", graph=graph) project = Project("project", "https://index.org/project/", index=index) self.assertEqual("https://index.org/project/", str(project.URL)) def test_Release(self) -> None: graph = PythonPackageDependencyGraph("graph") index = PythonPackageIndex("index", "https://index.org/", "https://api.index.org/v4/", graph=graph) project = Project("project", "https://index.org/project/", index=index) release = Release(PythonVersion.Parse("v1.0.0"), (now := datetime.now()), project=project) self.assertEqual(now, release.ReleasedAt) class PyPI(TestCase): def test_pyTooling(self) -> None: print() graph = PythonPackageDependencyGraph("pyTooling") pypi = PythonPackageIndex("PyPI", "https://pypi.org", "https://pypi.org/pypi/", graph=graph) project = pypi.DownloadProject("pyTooling", LazyLoaderState.PartiallyLoaded) self.assertEqual("pyTooling", project.Name) self.assertEqual("https://pypi.org/project/pyTooling/", str(project.URL)) self.assertGreaterEqual(len(project), 84) for release in project: self.assertEqual(project, release.Package) self.assertEqual(0, len(release)) def test_pyVersioning(self) -> None: print() graph = PythonPackageDependencyGraph("pyVersioning") pypi = PythonPackageIndex("PyPI", "https://pypi.org", "https://pypi.org/pypi/", graph=graph) project = pypi.DownloadProject("pyVersioning", LazyLoaderState.PartiallyLoaded) self.assertEqual("pyVersioning", project.Name) self.assertEqual("https://pypi.org/project/pyVersioning/", str(project.URL)) self.assertGreaterEqual(len(project), 39) for release in project: self.assertEqual(project, release.Package) self.assertEqual(0, len(release)) def test_SphinxReports(self) -> None: print() graph = PythonPackageDependencyGraph("sphinx-reports") pypi = PythonPackageIndex("PyPI", "https://pypi.org", "https://pypi.org/pypi/", graph=graph) project = pypi.DownloadProject("sphinx-reports", LazyLoaderState.PartiallyLoaded) self.assertEqual("sphinx-reports", project.Name) self.assertEqual("https://pypi.org/project/sphinx-reports/", str(project.URL)) self.assertGreaterEqual(len(project), 23) for release in project: print(f"{release!r}") self.assertEqual(project, release.Package) self.assertEqual(0, len(release)) pyTooling-8.11.0/tests/unit/Dependency/__init__.py000066400000000000000000000405271513317154500221120ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \ '_ \ / _ \ '_ \ / _` |/ _ \ '_ \ / __| | | | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, | # # |_| |___/ |___/ |_| |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for :mod:`pyTooling.Dependency`.""" from unittest import TestCase from pyTooling.Exceptions import ToolingException from pyTooling.Versioning import SemanticVersion from pyTooling.Dependency import PackageDependencyGraph, PackageStorage, Package, PackageVersion if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Graph(self) -> None: graph = PackageDependencyGraph("graph") self.assertEqual("graph", graph.Name) self.assertEqual(0, len(graph)) self.assertEqual(0, len(graph.Storages)) self.assertEqual("graph (empty)", str(graph)) def test_Storage(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) self.assertIs(graph, storage.Graph) self.assertEqual("storage", storage.Name) self.assertEqual(0, len(storage)) self.assertEqual(0, len(storage.Packages)) self.assertEqual("storage (empty)", str(storage)) def test_Package(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) package = Package("pack", storage=storage) self.assertEqual(1, len(graph)) self.assertEqual(1, len(graph.Storages)) self.assertIs(graph, storage.Graph) self.assertEqual(1, len(storage)) self.assertEqual(1, len(storage.Packages)) self.assertIs(storage, package.Storage) self.assertEqual("pack", package.Name) self.assertEqual(0, len(package)) self.assertEqual(0, len(package.Versions)) self.assertEqual("pack (empty)", str(package)) def test_CreatePackage(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) package = storage.CreatePackage("pack") self.assertEqual(1, len(storage)) self.assertEqual(1, len(storage.Packages)) self.assertIs(storage, package.Storage) self.assertEqual("pack", package.Name) self.assertEqual(0, len(package)) self.assertEqual(0, len(package.Versions)) self.assertEqual("pack (empty)", str(package)) def test_CreatePackages(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) for i, package in enumerate(storage.CreatePackages(( "pack1", "pack2", )), start=1): self.assertIs(storage, package.Storage) self.assertEqual(f"pack{i}", package.Name) self.assertEqual(0, len(package)) self.assertEqual(0, len(package.Versions)) self.assertEqual(f"pack{i} (empty)", str(package)) self.assertEqual(2, len(storage)) self.assertEqual(2, len(storage.Packages)) def test_PackageVersion(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) package = Package("pack", storage=storage) v10 = SemanticVersion.Parse("v1.0") v11 = SemanticVersion.Parse("v1.1") packageVersion10 = PackageVersion(v10, package) packageVersion11 = PackageVersion(v11, package) graph.SortPackageVersions() self.assertIs(package, packageVersion10.Package) self.assertIs(v10, packageVersion10.Version) self.assertEqual("pack - v1.0", str(packageVersion10)) self.assertIs(package, packageVersion11.Package) self.assertIs(v11, packageVersion11.Version) self.assertEqual("pack - v1.1", str(packageVersion11)) self.assertEqual("pack (latest: v1.1)", str(package)) def test_CreatePackageVersion(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) packageVersion = storage.CreatePackageVersion("pack", "v1.0") package = packageVersion.Package self.assertIs(storage, package.Storage) self.assertEqual("v1.0", packageVersion.Version) self.assertEqual("pack - v1.0", str(packageVersion)) def test_CreatePackageVersions(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) for i, packageVersion in enumerate(storage.CreatePackageVersions("pack", ( "v1.0", "v1.1", "v1.2", ))): package = packageVersion.Package self.assertIs(storage, package.Storage) self.assertEqual(f"v1.{i}", packageVersion.Version) self.assertEqual(f"pack - v1.{i}", str(packageVersion)) graph.SortPackageVersions() self.assertEqual("pack (latest: v1.2)", str(package)) class Construct(TestCase): def test_Package(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) package = storage.CreatePackage("pack") self.assertEqual("pack", package.Name) self.assertEqual(0, len(package)) self.assertEqual(0, len(package.Versions)) self.assertEqual("pack (empty)", str(package)) class DependsOn(TestCase): def test_ByObject(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) packageA = Package("packA", storage=storage) packageA_v10 = PackageVersion(pA_v10 := SemanticVersion.Parse("v1.0"), packageA) packageA_v11 = PackageVersion(pA_v11 := SemanticVersion.Parse("v1.1"), packageA) packageB = Package("packB", storage=storage) packageB_v10 = PackageVersion(pB_v10 := SemanticVersion.Parse("v1.0"), packageB) packageB_v20 = PackageVersion(pB_v20 := SemanticVersion.Parse("v2.0"), packageB) packageB_v21 = PackageVersion(pB_v21 := SemanticVersion.Parse("v2.1"), packageB) packageA_v10.AddDependencyTo(packageB, pB_v10) packageA_v11.AddDependencyTo(packageB, pB_v20) packageA_v11.AddDependencyTo(packageB, pB_v21) def test_ByName(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) root = storage.CreatePackageVersion("app", "v0.0") storage.CreatePackageVersions("packA", ( "v1.0", "v1.1" )) storage.CreatePackageVersions("packB", ( "v1.0", "v2.0", "v2.1" )) self.assertEqual(3, len(storage)) root.AddDependencyTo("packA", "v1.0") root.AddDependencyTo("packA", "v1.1") (pAv10 := storage["packA"]["v1.0"]).AddDependencyTo("packB", "v1.0") (pAv11 := storage["packA"]["v1.1"]).AddDependencyTo("packB", ("v2.0", "v2.1")) self.assertEqual(1, len(pAv10)) self.assertEqual(1, len(pAv11)) class SolveLatest(TestCase): def test_Simple(self) -> None: graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) root = storage.CreatePackageVersion("app", "v1.0") packA = storage.CreatePackageVersions("packA", ("v1.0", "v1.1", "v1.2")) root.AddDependencyToPackageVersions(packA) graph.SortPackageVersions() solution = root.SolveLatest() self.assertIn(root, solution) self.assertIn(packA[2], solution) def test_Advanced(self) -> None: print() graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) root = storage.CreatePackageVersion("app", "v0.0") packA = storage.CreatePackageVersions("packA", ( "v1.0", "v1.1" )) packB = storage.CreatePackageVersions("packB", ( "v1.0", "v2.0", "v2.1" )) packC = storage.CreatePackageVersions("packC", ( "v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v2.0", "v2.1" )) packD = storage.CreatePackageVersions("packD", ( "v1.0", "v2.0", "v3.0" )) root.AddDependencyTo("packA", ("v1.0", "v1.1")) root.AddDependencyTo("packC", ("v1.3", "v1.4", "v2.0", "v2.1")) storage["packA"]["v1.0"].AddDependencyTo("packB", "v1.0") storage["packA"]["v1.1"].AddDependencyTo("packB", ("v2.0", "v2.1")) storage["packC"]["v1.0"].AddDependencyTo("packD", "v1.0") storage["packC"]["v1.1"].AddDependencyTo("packD", "v1.0") storage["packC"]["v1.2"].AddDependencyTo("packD", ("v1.0", "v2.0")) storage["packC"]["v1.3"].AddDependencyTo("packD", ("v1.0", "v2.0")) storage["packC"]["v1.4"].AddDependencyTo("packD", ("v1.0", "v2.0", "v3.0")) storage["packC"]["v2.0"].AddDependencyTo("packD", ("v2.0", "v3.0")) storage["packC"]["v2.1"].AddDependencyTo("packD", "v3.0") graph.SortPackageVersions() solution = root.SolveLatest() self.assertIn(root, solution) self.assertIn(packA[1], solution) self.assertIn(packB[2], solution) self.assertIn(packC[6], solution) self.assertIn(packD[2], solution) def test_ConflictBacktracking(self) -> None: # Scenario: # * packA v2.0.0 (Latest) requires packB v2.0.0. # * But root only allows packB v1.0.0. # * Solver must reject packA v2.0.0 and pick packA v1.0.0 instead. graph = PackageDependencyGraph("graph") storage = PackageStorage("storage", graph=graph) root = storage.CreatePackageVersion("app", "v1.0") storage.CreatePackageVersions("packA", ("v1.0", "v2.0")) storage.CreatePackageVersions("packB", ("v1.0", "v2.0")) root.AddDependencyTo("packA", "v1.0") root.AddDependencyTo("packA", "v2.0") root.AddDependencyTo("packB", "v1.0") storage["packA"]["v2.0"].AddDependencyTo("packB", "v2.0") storage["packA"]["v1.0"].AddDependencyTo("packB", "v1.0") graph.SortPackageVersions() solution = {pv.Package.Name: pv.Version for pv in root.SolveLatest()} self.assertEqual(len(solution), 3) self.assertEqual(solution["app"], "v1.0") self.assertEqual(solution["packA"], "v1.0") self.assertEqual(solution["packB"], "v1.0") def test_CircularDependency(self) -> None: graph = PackageDependencyGraph("Circular") storage = PackageStorage("storage", graph=graph) root = storage.CreatePackageVersion("app", "v1.0") pAv10 = storage.CreatePackageVersion("packA", "v1.0") pBv10 = storage.CreatePackageVersion("packB", "v1.0") root.AddDependencyToPackageVersion(pAv10) pAv10.AddDependencyToPackageVersion(pBv10) pBv10.AddDependencyToPackageVersion(pAv10) graph.SortPackageVersions() solution = {pv.Package.Name: pv.Version for pv in root.SolveLatest()} self.assertEqual(len(solution), 3) self.assertEqual(solution["app"], "v1.0") self.assertEqual(solution["packA"], "v1.0") self.assertEqual(solution["packB"], "v1.0") def test_ComplexSuccessWithBacktracking(self) -> None: # Created by Google Gemini graph = PackageDependencyGraph("ComplexSuccess") storage = PackageStorage("storage", graph=graph) packages = storage.CreatePackages([f"pack{i}" for i in range(10)]) for package in packages: storage.CreatePackageVersions(package.Name, ("v1.0", "v1.1", "v1.2", "v1.3")) root = storage["pack0"]["v1.3"] # 1. Greedy Path: Root prefers latest of pack1, pack2, pack3 root.AddDependencyTo("pack1", "v1.3") root.AddDependencyTo("pack1", "v1.2") root.AddDependencyTo("pack2", "v1.3") root.AddDependencyTo("pack2", "v1.2") root.AddDependencyTo("pack3", "v1.3") # 2. The Conflict: pack1 v1.3.0 requires pack9 v1.3.0 storage["pack1"]["v1.3"].AddDependencyTo("pack9", "v1.3") # 3. The Constraint: pack3 v1.3.0 is older-leaning; it requires pack9 v1.0.0 # This forces the solver to backtrack from pack1 v1.3.0 to pack1 v1.2.0 storage["pack3"]["v1.3"].AddDependencyTo("pack9", "v1.0") # 4. Deep Dependency: pack1 v1.2.0 depends on pack4, which depends on pack5... storage["pack1"]["v1.2"].AddDependencyTo("pack4", "v1.3") storage["pack4"]["v1.3"].AddDependencyTo("pack5", "v1.3") # 5. Second Backtrack: pack2 v1.3.0 requires pack5 v1.1.0 # But pack4 v1.3.0 already locked in pack5 v1.3.0. # Solver must backtrack pack2 from 1.3.0 to 1.2.0. storage["pack2"]["v1.3"].AddDependencyTo("pack5", "v1.1") storage["pack2"]["v1.2"].AddDependencyTo("pack5", "v1.3") graph.SortPackageVersions() solution = {pv.Package.Name: pv.Version for pv in root.SolveLatest()} self.assertEqual(len(solution), 7) self.assertEqual(solution["pack0"], "v1.3") self.assertEqual(solution["pack1"], "v1.2") # Successful backtrack 1 self.assertEqual(solution["pack2"], "v1.2") # Successful backtrack 2 def test_ComplexFailureDeepConflict(self) -> None: # Created by Google Gemini graph = PackageDependencyGraph("ComplexFailure") storage = PackageStorage("storage", graph=graph) packages = storage.CreatePackages([f"pack{i}" for i in range(10)]) for package in packages: storage.CreatePackageVersions(package.Name, ("v1.0", "v1.1", "v1.2", "v1.3")) root = storage["pack0"]["v1.3"] # Chain of dependencies: # pack0 -> pack1 -> pack2 -> pack3 -> pack4 -> pack5 -> pack6 -> pack7 -> pack8 -> pack9 # Each allows multiple versions to keep the search space large # But, every version of pack8 (which is deep in the chain) requires pack9 to be v1.0.0 for i in range(9): fromPackage = storage[f"pack{i}"] toPackage = storage[f"pack{i + 1}"] for fromPackageVersion in (fromPackage[f"v1.{v}"] for v in range(4)): if i == 8: fromPackageVersion.AddDependencyToPackageVersion(toPackage["v1.0"]) else: for toPackageVersion in (toPackage[f"v1.{v}"] for v in range(4)): fromPackageVersion.AddDependencyToPackageVersion(toPackageVersion) # The impossible constraint: # Root requires pack9 to be v1.3.0 root.AddDependencyTo("pack9", "v1.3") # This will attempt every combination of pack1..pack8 before failing graph.SortPackageVersions() with self.assertRaises(ToolingException) as ex: _ = root.SolveLatest() self.assertIn("Could not resolve dependencies", str(ex.exception)) pyTooling-8.11.0/tests/unit/Exceptions/000077500000000000000000000000001513317154500200345ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Exceptions/Exceptions.py000066400000000000000000000120331513317154500225260ustar00rootroot00000000000000# ==================================================================================================================== # # # # _____ _ _ _____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | ____|_ _____ ___ _ __ | |_(_) ___ _ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | _| \ \/ / __/ _ \ '_ \| __| |/ _ \| '_ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___ > < (_| __/ |_) | |_| | (_) | | | \__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____/_/\_\___\___| .__/ \__|_|\___/|_| |_|___/ # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for :mod:`pyTooling.Exceptions`. """ from unittest import TestCase from pyTooling.Exceptions import EnvironmentException, PlatformNotSupportedException, NotConfiguredException if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) def raise_EnvironmentExecption() -> None: raise EnvironmentException("Environment does not provide 'PATH'.") def raise_PlatformNotSupportedException() -> None: raise PlatformNotSupportedException("Platform 'macOS' is not supported.") def raise_NotConfiguredException() -> None: raise NotConfiguredException("Option 'WorkingDirectory' is not specified in the configuration file.") class Exceptions(TestCase): def test_EnvironmentException(self) -> None: with self.assertRaises(EnvironmentException): raise_EnvironmentExecption() # self.assertEqual(context.exception.message, "Environment does not provide 'PATH'.") def test_PlatformNotSupportedException(self) -> None: with self.assertRaises(PlatformNotSupportedException): raise_PlatformNotSupportedException() # self.assertEqual(context.exception.message, "Platform 'OSX' is not supported.") def test_NotConfiguredException(self) -> None: with self.assertRaises(NotConfiguredException): raise_NotConfiguredException() # self.assertEqual(context.exception.message, "Option 'WorkingDirectory' is not specified in the configuration file.") pyTooling-8.11.0/tests/unit/Filesystem/000077500000000000000000000000001513317154500200375ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Filesystem/__init__.py000066400000000000000000000252541513317154500221600ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | ___(_) | ___ ___ _ _ ___| |_ ___ _ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_ | | |/ _ \/ __| | | / __| __/ _ \ '_ ` _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| _| | | | __/\__ \ |_| \__ \ || __/ | | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|_|\___||___/\__, |___/\__\___|_| |_| |_| # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for pyTooling.Tree.""" from pathlib import Path from typing import Any, Optional as Nullable, List, Tuple, Dict from unittest import TestCase from pyTooling.Exceptions import ToolingException from pyTooling.Common import count from pyTooling.Filesystem import Root, Directory, Filename, File if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Root(self) -> None: rootPath = Path("tests/data") root = Root(rootPath, collectSubdirectories=False) self.assertIs(root, root.Root) self.assertIsNone(root.Parent) self.assertEqual("data", root.Name) self.assertEqual(0, count(root.Subdirectories)) self.assertEqual(0, count(root.Files)) self.assertEqual(0, root.TotalSubdirectoryCount) self.assertEqual(0, root.TotalFileCount) def test_Directory(self) -> None: directory = Directory("directory") self.assertIsNone(directory.Root) self.assertIsNone(directory.Parent) self.assertEqual("directory", directory.Name) self.assertEqual(0, count(directory.Subdirectories)) self.assertEqual(0, count(directory.Files)) self.assertEqual(0, directory.TotalSubdirectoryCount) self.assertEqual(0, directory.TotalFileCount) def test_Directory_Root(self) -> None: rootPath = Path("tests/data") root = Root(rootPath, collectSubdirectories=False) directory = Directory("directory", parent=root) self.assertIs(root, root.Root) self.assertIsNone(root.Parent) self.assertEqual("data", root.Name) self.assertEqual(1, count(root.Subdirectories)) self.assertEqual(0, count(root.Files)) self.assertEqual(1, root.TotalSubdirectoryCount) self.assertEqual(0, root.TotalFileCount) self.assertIs(root, directory.Root) self.assertIs(root, directory.Parent) self.assertEqual("directory", directory.Name) self.assertEqual(0, count(directory.Subdirectories)) self.assertEqual(0, count(directory.Files)) self.assertEqual(0, directory.TotalSubdirectoryCount) self.assertEqual(0, directory.TotalFileCount) def test_Filename(self) -> None: filename = Filename("filename") self.assertIsNone(filename.Root) self.assertIsNone(filename.Parent) self.assertEqual("filename", filename.Name) self.assertIsNone(filename.File) with self.assertRaises(ToolingException): _ = filename.Size def test_Filename_Directory(self) -> None: directory = Directory("directory") filename = Filename("filename", parent=directory) self.assertIsNone(directory.Root) self.assertIsNone(directory.Parent) self.assertEqual("directory", directory.Name) self.assertEqual(0, count(directory.Subdirectories)) self.assertEqual(1, count(directory.Files)) self.assertEqual(0, directory.TotalSubdirectoryCount) self.assertEqual(1, directory.TotalFileCount) self.assertIsNone(filename.Root) self.assertIs(directory, filename.Parent) self.assertEqual("filename", filename.Name) self.assertIsNone(filename.File) with self.assertRaises(ToolingException): _ = filename.Size def test_File(self) -> None: file = File(1, 1024) self.assertIsNone(file.Root) self.assertEqual(0, len(file.Parents)) self.assertEqual(1, file.ID) self.assertEqual(1024, file.Size) def test_File_Filename(self) -> None: filename = Filename("filename") file = File(2, 2048, parent=filename) self.assertIsNone(filename.Root) self.assertIsNone(filename.Parent) self.assertEqual(2048, filename.Size) self.assertIsNone(file.Root) self.assertEqual(1, len(file.Parents)) self.assertListEqual(file.Parents, [filename]) self.assertEqual(2, file.ID) self.assertEqual(2048, file.Size) def test_TopDown(self) -> None: rootPath = Path("tests/data") root = Root(rootPath, collectSubdirectories=False) directory = Directory("directory", parent=root) filename = Filename("filename", parent=directory) file = File(2, 2048, parent=filename) self.assertIs(root, root.Root) self.assertIsNone(root.Parent) self.assertEqual("data", root.Name) self.assertEqual(1, count(root.Subdirectories)) self.assertEqual(0, count(root.Files)) self.assertEqual(1, root.TotalSubdirectoryCount) self.assertEqual(1, root.TotalFileCount) self.assertIs(root, directory.Root) self.assertIs(root, directory.Parent) self.assertEqual("directory", directory.Name) self.assertEqual(0, count(directory.Subdirectories)) self.assertEqual(1, count(directory.Files)) self.assertEqual(0, directory.TotalSubdirectoryCount) self.assertEqual(1, directory.TotalFileCount) self.assertIs(root, filename.Root) self.assertIs(directory, filename.Parent) self.assertEqual(2048, filename.Size) self.assertIs(root, file.Root) self.assertEqual(1, len(file.Parents)) self.assertListEqual(file.Parents, [filename]) self.assertEqual(2, file.ID) self.assertEqual(2048, file.Size) def test_BottomUp_ChainUp(self) -> None: rootPath = Path("tests/data") file = File(2, 2048) filename = Filename("filename") directory = Directory("directory") root = Root(rootPath, collectSubdirectories=False) file.AddParent(filename) filename.Parent = directory directory.Parent = root self.assertIs(root, root.Root) self.assertIsNone(root.Parent) self.assertEqual("data", root.Name) self.assertEqual(1, count(root.Subdirectories)) self.assertEqual(0, count(root.Files)) self.assertEqual(1, root.TotalSubdirectoryCount) self.assertEqual(1, root.TotalFileCount) self.assertIs(root, directory.Root) self.assertIs(root, directory.Parent) self.assertEqual("directory", directory.Name) self.assertEqual(0, count(directory.Subdirectories)) self.assertEqual(1, count(directory.Files)) self.assertEqual(0, directory.TotalSubdirectoryCount) self.assertEqual(1, directory.TotalFileCount) self.assertIs(root, filename.Root) self.assertIs(directory, filename.Parent) self.assertEqual(2048, filename.Size) self.assertIs(root, file.Root) self.assertEqual(1, len(file.Parents)) self.assertListEqual(file.Parents, [filename]) self.assertEqual(2, file.ID) self.assertEqual(2048, file.Size) def test_BottomUp_ChainDown(self) -> None: rootPath = Path("tests/data") file = File(2, 2048) filename = Filename("filename") directory = Directory("directory") root = Root(rootPath, collectSubdirectories=False) directory.Parent = root filename.Parent = directory file.AddParent(filename) self.assertIs(root, root.Root) self.assertIsNone(root.Parent) self.assertEqual("data", root.Name) self.assertEqual(1, count(root.Subdirectories)) self.assertEqual(0, count(root.Files)) self.assertEqual(1, root.TotalSubdirectoryCount) self.assertEqual(1, root.TotalFileCount) self.assertIs(root, directory.Root) self.assertIs(root, directory.Parent) self.assertEqual("directory", directory.Name) self.assertEqual(0, count(directory.Subdirectories)) self.assertEqual(1, count(directory.Files)) self.assertEqual(0, directory.TotalSubdirectoryCount) self.assertEqual(1, directory.TotalFileCount) self.assertIs(root, filename.Root) self.assertIs(directory, filename.Parent) self.assertEqual(2048, filename.Size) self.assertIs(root, file.Root) self.assertEqual(1, len(file.Parents)) self.assertListEqual(file.Parents, [filename]) self.assertEqual(2, file.ID) self.assertEqual(2048, file.Size) pyTooling-8.11.0/tests/unit/Graph/000077500000000000000000000000001513317154500167545ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Graph/GraphML.py000066400000000000000000000276611513317154500206340ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for pyTooling.Graph.GraphML.""" from unittest import TestCase from pyTooling.Graph import Graph as pyTooling_Graph, Subgraph as pyTooling_Subgraph, Vertex from pyTooling.Graph.GraphML import AttributeContext, AttributeTypes, Key, Data, Node, Edge, Graph, Subgraph, GraphMLDocument from pyTooling.Tree import Node as pyToolingNode if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Construction(TestCase): def test_Key(self) -> None: key = Key("k1", AttributeContext.Node, "color", AttributeTypes.String) self.assertEqual("k1", key.ID) self.assertEqual("color", str(key.AttributeName)) self.assertEqual("string", str(key.AttributeType)) self.assertFalse(key.HasClosingTag) self.assertEqual("""\n""", key.Tag(0)) self.assertListEqual([ """\n""" ], key.ToStringLines(0)) print() for line in key.ToStringLines(): print(line, end="") def test_Data(self) -> None: key = Key("k1", AttributeContext.Node, "color", AttributeTypes.String) data = Data(key, "violet") self.assertEqual(key, data.Key) self.assertEqual("violet", data.Data) self.assertFalse(data.HasClosingTag) self.assertEqual("""violet\n""", data.Tag(0)) self.assertListEqual([ """violet\n""" ], data.ToStringLines(0)) print() for line in data.ToStringLines(): print(line, end="") def test_Node(self) -> None: node = Node("n1") self.assertEqual("n1", node.ID) self.assertFalse(node.HasClosingTag) self.assertEqual("""\n""", node.Tag(0)) self.assertListEqual([ """\n""" ], node.ToStringLines(0)) print() for line in node.ToStringLines(): print(line, end="") def test_NodeWithData(self) -> None: key = Key("k1", AttributeContext.Node, "color", AttributeTypes.String) node = Node("n1") node.AddData(Data(key, "violet")) self.assertEqual("n1", node.ID) self.assertTrue(node.HasClosingTag) self.assertEqual("""\n""", node.OpeningTag(0)) self.assertEqual("""\n""", node.ClosingTag(0)) self.assertListEqual([ """\n""", """ violet\n""", """\n""" ], node.ToStringLines(0)) print() for line in node.ToStringLines(): print(line, end="") def test_Edge(self) -> None: node1 = Node("n1") node2 = Node("n2") edge = Edge("e1", node1, node2) self.assertFalse(edge.HasClosingTag) self.assertEqual("""\n""", edge.Tag(0)) self.assertListEqual([ """\n""" ], edge.ToStringLines(0)) print() for line in edge.ToStringLines(): print(line, end="") def test_EdgeWithData(self) -> None: key = Key("k1", AttributeContext.Node, "color", AttributeTypes.String) node1 = Node("n1") node2 = Node("n2") edge = Edge("e1", node1, node2) edge.AddData(Data(key, "violet")) self.assertEqual("e1", edge.ID) self.assertTrue(edge.HasClosingTag) self.assertEqual("""\n""", edge.OpeningTag(0)) self.assertEqual("""\n""", edge.ClosingTag(0)) self.assertListEqual([ """\n""", """ violet\n""", """\n""" ], edge.ToStringLines(0)) print() for line in edge.ToStringLines(): print(line, end="") def test_Graph(self) -> None: graph = Graph(None, "g1") self.assertTrue(graph.HasClosingTag) self.assertEqual("""\ \n""", graph.OpeningTag(0)) self.assertEqual("""\n""", graph.ClosingTag(0)) self.assertListEqual([ """\ \n""", """\n""" ], graph.ToStringLines(0)) print() for line in graph.ToStringLines(): print(line, end="") def test_GraphWithNodesAndEdges(self) -> None: graph = Graph(None, "g1") graph.AddNode(Node("n1")) graph.AddNode(Node("n2")) graph.AddEdge(Edge("e1", graph.GetNode("n1"), graph.GetNode("n2"))) self.assertTrue(graph.HasClosingTag) self.assertEqual("""\ \n""", graph.OpeningTag(0)) self.assertEqual("""\n""", graph.ClosingTag(0)) self.assertListEqual([ """\ \n""", """ \n""", """ \n""", """ \n""", """\n""" ], graph.ToStringLines(0)) print() for line in graph.ToStringLines(): print(line, end="") def test_GraphWithSubgraph(self) -> None: graph = Graph(None, "g1") graph.AddNode(Node("n1")) graph.AddNode(Node("n2")) graph.AddEdge(Edge("e1", graph.GetNode("n1"), graph.GetNode("n2"))) sg1 = graph.AddSubgraph(Subgraph("nsg1", "sg1")) sg1.AddNode(Node("sg1n1")) sg1.AddNode(Node("sg1n2")) sg1.AddEdge(Edge("sg1e1", sg1.GetNode("sg1n1"), sg1.GetNode("sg1n2"))) sg2 = graph.AddSubgraph(Subgraph("nsg2", "sg2")) sg2.AddNode(Node("sg2n1")) sg2.AddNode(Node("sg2n2")) sg2.AddEdge(Edge("sg2e1", sg2.GetNode("sg2n1"), sg2.GetNode("sg2n2"))) graph.AddEdge(Edge("e2", graph.GetNode("n1"), sg1.GetNode("sg1n2"))) graph.AddEdge(Edge("e3", graph.GetNode("n2"), sg2.GetNode("sg2n1"))) graph.AddEdge(Edge("e4", sg1.GetNode("sg1n1"), sg2.GetNode("sg2n2"))) self.assertTrue(graph.HasClosingTag) self.assertEqual(2, len(graph.Subgraphs)) print() for line in graph.ToStringLines(): print(line, end="") def test_GraphML(self) -> None: doc = GraphMLDocument("g1") self.assertIsInstance(doc._graph, Graph) print() for line in doc.ToStringLines(): print(line, end="") class pyToolingGraph(TestCase): def test_ConvertGraph(self) -> None: graph = pyTooling_Graph(name="g1") vertex1 = Vertex(vertexID="n1", value="v1", graph=graph) vertex2 = Vertex(vertexID="n2", value="v2", graph=graph) edge = vertex1.EdgeToVertex(vertex2, edgeValue="v12", edgeWeight=1) doc = GraphMLDocument() doc.FromGraph(graph) self.assertEqual("g1", doc._graph.ID) self.assertEqual(2, len(doc._graph._nodes)) self.assertEqual(1, len(doc._graph._edges)) print() for line in doc.ToStringLines(): print(line, end="") def test_ConvertSubgraph(self) -> None: graph = pyTooling_Graph(name="g1") subgraph1 = pyTooling_Subgraph(name="sg1", graph=graph) subgraph2 = pyTooling_Subgraph(name="sg2", graph=graph) vertex1 = Vertex(vertexID="n1", value="v1", graph=graph) vertex2 = Vertex(vertexID="n2", value="v2", graph=graph) vertex3 = Vertex(vertexID="n3", value="v3", subgraph=subgraph1) vertex4 = Vertex(vertexID="n4", value="v4", subgraph=subgraph1) vertex5 = Vertex(vertexID="n5", value="v5", subgraph=subgraph2) vertex6 = Vertex(vertexID="n6", value="v6", subgraph=subgraph2) edge12 = vertex1.EdgeToVertex(vertex2, edgeValue="v12", edgeWeight=1) edge34 = vertex3.EdgeToVertex(vertex4, edgeValue="v34", edgeWeight=1) edge56 = vertex5.EdgeToVertex(vertex6, edgeValue="v56", edgeWeight=1) link13 = vertex1.LinkToVertex(vertex3, linkValue="v13", linkWeight=2) link25 = vertex2.LinkToVertex(vertex5, linkValue="v25", linkWeight=2) link46 = vertex4.LinkToVertex(vertex6, linkValue="v46", linkWeight=2) doc = GraphMLDocument() doc.FromGraph(graph) self.assertEqual("g1", doc._graph.ID) self.assertEqual(2, len(doc._graph._subgraphs)) self.assertEqual(4, len(doc._graph._nodes)) self.assertEqual(1, len(doc._graph._edges)) print() for line in doc.ToStringLines(): print(line, end="") class pyToolingTree(TestCase): def test_Conversion(self) -> None: root = pyToolingNode(nodeID="n0", value="v0") child1 = pyToolingNode("n1", "v1", parent=root) child2 = pyToolingNode("n2", "v2", parent=root) doc = GraphMLDocument() doc.FromTree(root) self.assertEqual("n0", doc._graph.ID) self.assertEqual(3, len(doc._graph._nodes)) self.assertEqual(2, len(doc._graph._edges)) print() for line in doc.ToStringLines(): print(line, end="") pyTooling-8.11.0/tests/unit/Graph/__init__.py000066400000000000000000001374441513317154500211020ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # # |_| |___/ |___/ |_| # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for pyTooling.Graph.""" from typing import Any, Optional as Nullable, List, Tuple, Callable from unittest import TestCase from pyTooling.Decorators import readonly from pyTooling.Graph import Graph, Vertex, Edge, Link, Subgraph, View, DuplicateVertexError, CycleError from pyTooling.Graph import GraphException, DuplicateEdgeError, NotInSameGraph, DestinationNotReachable if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Construction(TestCase): def test_Graph(self) -> None: graph = Graph() self.assertIsNone(graph.Name) self.assertEqual(0, graph.VertexCount) self.assertEqual(0, graph.EdgeCount) self.assertEqual(0, graph.ComponentCount) self.assertEqual(0, graph.SubgraphCount) self.assertEqual(0, graph.ViewCount) self.assertEqual(0, len(graph)) self.assertEqual("", repr(graph)) self.assertEqual("Graph: unnamed graph", str(graph)) def test_GraphWithName(self) -> None: graph = Graph("myGraph") self.assertEqual("myGraph", graph.Name) self.assertEqual("", repr(graph)) self.assertEqual("Graph: 'myGraph'", str(graph)) def test_StandaloneVertex(self) -> None: root: Vertex[Nullable[Any], int, str, Any] = Vertex() self.assertIsNone(root.ID) self.assertIsNone(root.Value) self.assertEqual(1, root.Graph.VertexCount) self.assertEqual(1, root.Graph.ComponentCount) self.assertEqual("", repr(root)) self.assertEqual("", str(root)) def test_StandaloneEdge(self) -> None: vertex1 = Vertex() vertex2 = Vertex() with self.assertRaises(TypeError): Edge(1, vertex1) with self.assertRaises(TypeError): Edge(vertex1, 2) with self.assertRaises(TypeError): Edge(vertex1, vertex2, edgeID=[]) with self.assertRaises(TypeError): Edge(vertex1, vertex2, weight="2") with self.assertRaises(NotInSameGraph): Edge(vertex1, vertex2) def test_StandaloneLink(self) -> None: vertex1 = Vertex() vertex2 = Vertex() with self.assertRaises(TypeError): Link(1, vertex1) with self.assertRaises(TypeError): Link(vertex1, 2) with self.assertRaises(TypeError): Link(vertex1, vertex2, linkID=[]) with self.assertRaises(TypeError): Link(vertex1, vertex2, weight="2") with self.assertRaises(NotInSameGraph): Link(vertex1, vertex2) def test_SingleVertexForExistingGraph(self) -> None: graph = Graph() root: Vertex[Nullable[Any], int, str, Any] = Vertex(graph=graph) self.assertIsNone(root.ID) self.assertIsNone(root.Value) self.assertEqual(1, graph.VertexCount) self.assertEqual(1, graph.ComponentCount) def test_EdgeToVertex(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) self.assertEqual(2, graph.ComponentCount) edge12 = vertex1.EdgeToVertex(vertex2) self.assertEqual(1, graph.ComponentCount) self.assertEqual(1, graph.EdgeCount) self.assertEqual(1, vertex1.EdgeCount) self.assertEqual(1, vertex1.OutboundEdgeCount) self.assertEqual(0, vertex1.InboundEdgeCount) self.assertEqual(1, vertex2.EdgeCount) self.assertEqual(0, vertex2.OutboundEdgeCount) self.assertEqual(1, vertex2.InboundEdgeCount) self.assertTupleEqual(tuple(), vertex1.InboundEdges) self.assertTupleEqual((edge12,), vertex1.OutboundEdges) self.assertTupleEqual((edge12,), vertex2.InboundEdges) self.assertTupleEqual(tuple(), vertex2.OutboundEdges) self.assertTupleEqual(tuple(), vertex1.Predecessors) self.assertTupleEqual((vertex2,), vertex1.Successors) self.assertTupleEqual((vertex1,), vertex2.Predecessors) self.assertTupleEqual(tuple(), vertex2.Successors) self.assertTrue(vertex1.HasEdgeToDestination(vertex2)) self.assertFalse(vertex1.HasEdgeFromSource(vertex2)) self.assertFalse(vertex2.HasEdgeToDestination(vertex1)) self.assertTrue(vertex2.HasEdgeFromSource(vertex1)) self.assertTrue(vertex1.IsRoot) self.assertFalse(vertex1.IsLeaf) self.assertFalse(vertex2.IsRoot) self.assertTrue(vertex2.IsLeaf) self.assertIs(vertex1, edge12.Source) self.assertIs(vertex2, edge12.Destination) self.assertIsNone(edge12.ID) self.assertIsNone(edge12.Value) # self.assertEqual("", repr(edge12)) # self.assertEqual("", str(edge12)) def test_EdgeFromVertex(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) self.assertEqual(2, graph.ComponentCount) edge21 = vertex1.EdgeFromVertex(vertex2) self.assertEqual(1, graph.ComponentCount) self.assertEqual(1, graph.EdgeCount) self.assertEqual(1, vertex1.EdgeCount) self.assertEqual(0, vertex1.OutboundEdgeCount) self.assertEqual(1, vertex1.InboundEdgeCount) self.assertEqual(1, vertex2.EdgeCount) self.assertEqual(1, vertex2.OutboundEdgeCount) self.assertEqual(0, vertex2.InboundEdgeCount) self.assertTupleEqual((edge21,), vertex1.InboundEdges) self.assertTupleEqual(tuple(), vertex1.OutboundEdges) self.assertTupleEqual(tuple(), vertex2.InboundEdges) self.assertTupleEqual((edge21,), vertex2.OutboundEdges) self.assertTupleEqual((vertex2,), vertex1.Predecessors) self.assertTupleEqual(tuple(), vertex1.Successors) self.assertTupleEqual(tuple(), vertex2.Predecessors) self.assertTupleEqual((vertex1,), vertex2.Successors) self.assertFalse(vertex1.HasEdgeToDestination(vertex2)) self.assertTrue(vertex1.HasEdgeFromSource(vertex2)) self.assertTrue(vertex2.HasEdgeToDestination(vertex1)) self.assertFalse(vertex2.HasEdgeFromSource(vertex1)) self.assertFalse(vertex1.IsRoot) self.assertTrue(vertex1.IsLeaf) self.assertTrue(vertex2.IsRoot) self.assertFalse(vertex2.IsLeaf) self.assertIs(vertex1, edge21.Destination) self.assertIs(vertex2, edge21.Source) self.assertIsNone(edge21.ID) self.assertIsNone(edge21.Value) # self.assertEqual("", repr(edge12)) # self.assertEqual("", str(edge12)) def test_EdgeToNewVertex(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) self.assertEqual(1, graph.ComponentCount) edge1x = vertex1.EdgeToNewVertex() vertex2 = edge1x.Destination self.assertEqual(1, graph.ComponentCount) self.assertEqual(1, graph.EdgeCount) self.assertEqual(1, vertex1.EdgeCount) self.assertEqual(1, vertex1.OutboundEdgeCount) self.assertEqual(0, vertex1.InboundEdgeCount) self.assertEqual(1, vertex2.EdgeCount) self.assertEqual(0, vertex2.OutboundEdgeCount) self.assertEqual(1, vertex2.InboundEdgeCount) self.assertTupleEqual(tuple(), vertex1.InboundEdges) self.assertTupleEqual((edge1x,), vertex1.OutboundEdges) self.assertTupleEqual((edge1x,), vertex2.InboundEdges) self.assertTupleEqual(tuple(), vertex2.OutboundEdges) self.assertTupleEqual(tuple(), vertex1.Predecessors) self.assertTupleEqual((vertex2,), vertex1.Successors) self.assertTupleEqual((vertex1,), vertex2.Predecessors) self.assertTupleEqual(tuple(), vertex2.Successors) self.assertTrue(vertex1.HasEdgeToDestination(vertex2)) self.assertFalse(vertex1.HasEdgeFromSource(vertex2)) self.assertFalse(vertex2.HasEdgeToDestination(vertex1)) self.assertTrue(vertex2.HasEdgeFromSource(vertex1)) self.assertTrue(vertex1.IsRoot) self.assertFalse(vertex1.IsLeaf) self.assertFalse(vertex2.IsRoot) self.assertTrue(vertex2.IsLeaf) self.assertIs(vertex1, edge1x.Source) # self.assertIs(vertex2, edge1x.Destination) self.assertIsNone(edge1x.ID) self.assertIsNone(edge1x.Value) # self.assertEqual("", repr(edge12)) # self.assertEqual("", str(edge12)) def test_EdgeFromNewVertex(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) self.assertEqual(1, graph.ComponentCount) edgex1 = vertex1.EdgeFromNewVertex() vertex2 = edgex1.Source self.assertEqual(1, graph.ComponentCount) self.assertEqual(1, graph.EdgeCount) self.assertEqual(1, vertex1.EdgeCount) self.assertEqual(0, vertex1.OutboundEdgeCount) self.assertEqual(1, vertex1.InboundEdgeCount) self.assertEqual(1, vertex2.EdgeCount) self.assertEqual(1, vertex2.OutboundEdgeCount) self.assertEqual(0, vertex2.InboundEdgeCount) self.assertTupleEqual((edgex1,), vertex1.InboundEdges) self.assertTupleEqual(tuple(), vertex1.OutboundEdges) self.assertTupleEqual(tuple(), vertex2.InboundEdges) self.assertTupleEqual((edgex1,), vertex2.OutboundEdges) self.assertTupleEqual((vertex2,), vertex1.Predecessors) self.assertTupleEqual(tuple(), vertex1.Successors) self.assertTupleEqual(tuple(), vertex2.Predecessors) self.assertTupleEqual((vertex1,), vertex2.Successors) self.assertFalse(vertex1.HasEdgeToDestination(vertex2)) self.assertTrue(vertex1.HasEdgeFromSource(vertex2)) self.assertTrue(vertex2.HasEdgeToDestination(vertex1)) self.assertFalse(vertex2.HasEdgeFromSource(vertex1)) self.assertFalse(vertex1.IsRoot) self.assertTrue(vertex1.IsLeaf) self.assertTrue(vertex2.IsRoot) self.assertFalse(vertex2.IsLeaf) self.assertIs(vertex1, edgex1.Destination) # self.assertIs(vertex2, edgex1.Source) self.assertIsNone(edgex1.ID) self.assertIsNone(edgex1.Value) # self.assertEqual("", repr(edge12)) # self.assertEqual("", str(edge12)) def test_LinkToVertex(self) -> None: graph = Graph() subgraph1 = Subgraph(graph=graph) subgraph2 = Subgraph(graph=graph) vertex1 = Vertex(subgraph=subgraph1) vertex2 = Vertex(subgraph=subgraph2) self.assertEqual(2, graph.ComponentCount) link12 = vertex1.LinkToVertex(vertex2) self.assertEqual(1, graph.ComponentCount) self.assertEqual(0, graph.LinkCount) self.assertEqual(1, subgraph1.LinkCount) self.assertEqual(1, subgraph2.LinkCount) self.assertEqual(1, vertex1.LinkCount) self.assertEqual(1, vertex1.OutboundLinkCount) self.assertEqual(0, vertex1.InboundLinkCount) self.assertEqual(1, vertex2.LinkCount) self.assertEqual(0, vertex2.OutboundLinkCount) self.assertEqual(1, vertex2.InboundLinkCount) self.assertTupleEqual(tuple(), vertex1.InboundLinks) self.assertTupleEqual((link12,), vertex1.OutboundLinks) self.assertTupleEqual((link12,), vertex2.InboundLinks) self.assertTupleEqual(tuple(), vertex2.OutboundLinks) # self.assertTupleEqual(tuple(), vertex1.Predecessors) # self.assertTupleEqual((vertex2,), vertex1.Successors) # self.assertTupleEqual((vertex1,), vertex2.Predecessors) # self.assertTupleEqual(tuple(), vertex2.Successors) self.assertTrue(vertex1.HasLinkToDestination(vertex2)) self.assertFalse(vertex1.HasLinkFromSource(vertex2)) self.assertFalse(vertex2.HasLinkToDestination(vertex1)) self.assertTrue(vertex2.HasLinkFromSource(vertex1)) # self.assertTrue(vertex1.IsRoot) # self.assertFalse(vertex1.IsLeaf) # self.assertFalse(vertex2.IsRoot) # self.assertTrue(vertex2.IsLeaf) self.assertIs(vertex1, link12.Source) self.assertIs(vertex2, link12.Destination) self.assertIsNone(link12.ID) self.assertIsNone(link12.Value) # self.assertEqual("", repr(link12)) # self.assertEqual("", str(link12)) def test_LinkFromVertex(self) -> None: graph = Graph() subgraph1 = Subgraph(graph=graph) subgraph2 = Subgraph(graph=graph) vertex1 = Vertex(subgraph=subgraph1) vertex2 = Vertex(subgraph=subgraph2) self.assertEqual(2, graph.ComponentCount) link21 = vertex1.LinkFromVertex(vertex2) self.assertEqual(1, graph.ComponentCount) self.assertEqual(0, graph.LinkCount) self.assertEqual(1, subgraph1.LinkCount) self.assertEqual(1, subgraph2.LinkCount) self.assertEqual(1, vertex1.LinkCount) self.assertEqual(0, vertex1.OutboundLinkCount) self.assertEqual(1, vertex1.InboundLinkCount) self.assertEqual(1, vertex2.LinkCount) self.assertEqual(1, vertex2.OutboundLinkCount) self.assertEqual(0, vertex2.InboundLinkCount) self.assertTupleEqual((link21,), vertex1.InboundLinks) self.assertTupleEqual(tuple(), vertex1.OutboundLinks) self.assertTupleEqual(tuple(), vertex2.InboundLinks) self.assertTupleEqual((link21,), vertex2.OutboundLinks) # self.assertTupleEqual((vertex2,), vertex1.Predecessors) # self.assertTupleEqual(tuple(), vertex1.Successors) # self.assertTupleEqual(tuple(), vertex2.Predecessors) # self.assertTupleEqual((vertex1,), vertex2.Successors) self.assertFalse(vertex1.HasLinkToDestination(vertex2)) self.assertTrue(vertex1.HasLinkFromSource(vertex2)) self.assertTrue(vertex2.HasLinkToDestination(vertex1)) self.assertFalse(vertex2.HasLinkFromSource(vertex1)) # self.assertFalse(vertex1.IsRoot) # self.assertTrue(vertex1.IsLeaf) # self.assertTrue(vertex2.IsRoot) # self.assertFalse(vertex2.IsLeaf) self.assertIs(vertex1, link21.Destination) self.assertIs(vertex2, link21.Source) self.assertIsNone(link21.ID) self.assertIsNone(link21.Value) # self.assertEqual("", repr(link12)) # self.assertEqual("", str(link12)) def test_Subgraph(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) self.assertEqual(1, graph.SubgraphCount) self.assertIs(graph, subgraph.Graph) self.assertIsNone(subgraph.Name) self.assertEqual(0, subgraph.VertexCount) def test_SubgraphWithName(self) -> None: graph = Graph() subgraph = Subgraph(name="subgraph1", graph=graph) self.assertEqual(1, graph.SubgraphCount) self.assertIs(graph, subgraph.Graph) self.assertEqual("subgraph1", subgraph.Name) self.assertEqual(0, subgraph.VertexCount) def test_View(self) -> None: graph = Graph() view = View(graph=graph) self.assertEqual(1, graph.ViewCount) self.assertIs(graph, view.Graph) self.assertIsNone(view.Name) self.assertEqual(0, view.VertexCount) def test_ViewWithName(self) -> None: graph = Graph() view = View(name="view1", graph=graph) self.assertEqual(1, graph.ViewCount) self.assertIs(graph, view.Graph) self.assertEqual("view1", view.Name) self.assertEqual(0, view.VertexCount) def test_SimpleTree(self) -> None: v1 = Vertex() v11 = v1.EdgeToNewVertex().Destination v111 = v11.EdgeToNewVertex().Destination v112 = v11.EdgeToNewVertex().Destination v12 = v1.EdgeToNewVertex().Destination v121 = v12.EdgeToNewVertex().Destination v1211 = v121.EdgeToNewVertex().Destination self.assertEqual(2, v1.OutboundEdgeCount) self.assertEqual(2, v11.OutboundEdgeCount) self.assertEqual(0, v111.OutboundEdgeCount) self.assertEqual(0, v112.OutboundEdgeCount) self.assertEqual(1, v12.OutboundEdgeCount) self.assertEqual(1, v121.OutboundEdgeCount) self.assertEqual(0, v1211.OutboundEdgeCount) self.assertEqual(7, v1.Graph.VertexCount) self.assertEqual(6, v1.Graph.EdgeCount) self.assertEqual(1, v1.Graph.ComponentCount) self.assertEqual(7, next(iter(v1.Graph.Components)).VertexCount) class Subgraphs(TestCase): def test_OuterVertices(self) -> None: graph = Graph() subgraph1 = Subgraph(name="subgraph1", graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) self.assertEqual(1, graph.SubgraphCount) self.assertEqual(2, graph.VertexCount) self.assertEqual(0, subgraph1.VertexCount) def test_InnerVertices(self) -> None: graph = Graph() subgraph1 = Subgraph(name="subgraph1", graph=graph) vertex3 = Vertex(subgraph=subgraph1) vertex4 = Vertex(subgraph=subgraph1) self.assertEqual(1, graph.SubgraphCount) self.assertEqual(0, graph.VertexCount) self.assertEqual(2, subgraph1.VertexCount) def test_OuterAndInnerVertices(self) -> None: graph = Graph() subgraph1 = Subgraph(name="subgraph1", graph=graph) subgraph2 = Subgraph(name="subgraph2", graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph1) vertex4 = Vertex(subgraph=subgraph1) vertex5 = Vertex(subgraph=subgraph2) vertex6 = Vertex(subgraph=subgraph2) self.assertEqual(2, graph.SubgraphCount) self.assertEqual(2, graph.VertexCount) self.assertEqual(2, subgraph1.VertexCount) self.assertEqual(2, subgraph2.VertexCount) def test_OuterEdges(self) -> None: graph = Graph() subgraph1 = Subgraph(name="subgraph1", graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(graph=graph) edge12 = vertex1.EdgeToVertex(vertex2) edge23 = vertex2.EdgeToVertex(vertex3) edge31 = vertex3.EdgeToVertex(vertex1) self.assertEqual(1, graph.SubgraphCount) self.assertEqual(3, graph.VertexCount) self.assertEqual(3, graph.EdgeCount) self.assertEqual(0, subgraph1.VertexCount) self.assertEqual(0, subgraph1.EdgeCount) def test_InnerEdges(self) -> None: graph = Graph() subgraph1 = Subgraph(name="subgraph1", graph=graph) vertex1 = Vertex(subgraph=subgraph1) vertex2 = Vertex(subgraph=subgraph1) vertex3 = Vertex(subgraph=subgraph1) edge12 = vertex1.EdgeToVertex(vertex2) edge23 = vertex2.EdgeToVertex(vertex3) edge31 = vertex3.EdgeToVertex(vertex1) self.assertEqual(1, graph.SubgraphCount) self.assertEqual(0, graph.VertexCount) self.assertEqual(0, graph.EdgeCount) self.assertEqual(3, subgraph1.VertexCount) self.assertEqual(3, subgraph1.EdgeCount) def test_OuterToInnerEdges(self) -> None: graph = Graph() subgraph1 = Subgraph(name="subgraph1", graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(subgraph=subgraph1) with self.assertRaises(GraphException): edge12 = vertex1.EdgeToVertex(vertex2) link12 = vertex1.LinkToVertex(vertex2) self.assertEqual(1, graph.SubgraphCount) self.assertEqual(1, graph.VertexCount) self.assertEqual(0, graph.EdgeCount) self.assertEqual(1, subgraph1.VertexCount) self.assertEqual(1, vertex1.LinkCount) self.assertEqual(1, vertex1.OutboundLinkCount) self.assertEqual(0, vertex1.InboundLinkCount) self.assertEqual(1, vertex2.LinkCount) self.assertEqual(0, vertex2.OutboundLinkCount) self.assertEqual(1, vertex2.InboundLinkCount) self.assertTupleEqual((link12,), vertex1.OutboundLinks) self.assertTupleEqual(tuple(), vertex1.InboundLinks) self.assertTupleEqual(tuple(), vertex2.OutboundLinks) self.assertTupleEqual((link12,), vertex2.InboundLinks) class Names(TestCase): def test_Graph_NoName(self) -> None: graph = Graph() self.assertIsNone(graph.Name) graph.Name = "myGraph" self.assertEqual("myGraph", graph.Name) def test_Graph_WrongName(self) -> None: with self.assertRaises(TypeError): graph = Graph(name=25) def test_Graph_WithName(self) -> None: graph = Graph(name="myGraph") self.assertEqual("myGraph", graph.Name) with self.assertRaises(TypeError): graph.Name = None with self.assertRaises(TypeError): graph.Name = 25 class IDs(TestCase): def test_VertexNoneID(self) -> None: graph = Graph() vertex = Vertex(graph=graph) self.assertIsNone(vertex.ID) with self.assertRaises(AttributeError): vertex.ID = 5 self.assertIsNone(vertex.ID) self.assertIs(vertex, graph.GetVertexByID(None)) def test_VertexID(self) -> None: graph = Graph() vertex = Vertex(vertexID=1, graph=graph) self.assertEqual(1, vertex.ID) self.assertIs(vertex, graph.GetVertexByID(1)) def test_HasVertexByNoneID(self) -> None: graph = Graph() self.assertFalse(graph.HasVertexByID(None)) _ = Vertex(graph=graph) self.assertTrue(graph.HasVertexByID(None)) def test_HasVertexByID(self) -> None: graph = Graph() self.assertFalse(graph.HasVertexByID(1)) _ = Vertex(vertexID=1, graph=graph) self.assertFalse(graph.HasVertexByID(None)) self.assertFalse(graph.HasVertexByID(0)) self.assertTrue(graph.HasVertexByID(1)) def test_GetVertexByNoneID(self) -> None: graph = Graph() with self.assertRaises(KeyError): _ = graph.GetVertexByID(None) vertex = Vertex(graph=graph) self.assertIs(vertex, graph.GetVertexByID(None)) vertex = Vertex(graph=graph) with self.assertRaises(KeyError): _ = graph.GetVertexByID(None) def test_GetVertexByID(self) -> None: graph = Graph() with self.assertRaises(KeyError): _ = graph.GetVertexByID(1) vertex = Vertex(vertexID=1, graph=graph) self.assertIs(vertex, graph.GetVertexByID(1)) with self.assertRaises(DuplicateVertexError): _ = Vertex(vertexID=1, graph=graph) class Values(TestCase): def test_VertexNoneValue(self) -> None: graph = Graph() vertex = Vertex(graph=graph) self.assertIsNone(vertex.Value) vertex.Value = 5 self.assertEqual(5, vertex.Value) self.assertIs(vertex, graph.GetVertexByValue(5)) def test_VertexValue(self) -> None: graph = Graph() vertex = Vertex(value=1, graph=graph) self.assertEqual(1, vertex.Value) vertex.Value = None self.assertIsNone(vertex.Value) self.assertIs(vertex, graph.GetVertexByValue(None)) def test_HasVertexByNoneValue(self) -> None: graph = Graph() self.assertFalse(graph.HasVertexByValue(None)) _ = Vertex(graph=graph) self.assertTrue(graph.HasVertexByValue(None)) def test_HasVertexByValue(self) -> None: graph = Graph() self.assertFalse(graph.HasVertexByValue(1)) vertex = Vertex(value=1, graph=graph) self.assertFalse(graph.HasVertexByValue(None)) self.assertFalse(graph.HasVertexByValue(0)) self.assertTrue(graph.HasVertexByValue(1)) vertex.Value = None self.assertTrue(graph.HasVertexByValue(None)) self.assertFalse(graph.HasVertexByValue(1)) def test_GetVertexByNoneValue(self) -> None: graph = Graph() with self.assertRaises(KeyError): _ = graph.GetVertexByValue(None) vertex = Vertex(graph=graph) self.assertIs(vertex, graph.GetVertexByValue(None)) vertex = Vertex(graph=graph) with self.assertRaises(KeyError): _ = graph.GetVertexByValue(None) def test_GetVertexByValue(self) -> None: graph = Graph() with self.assertRaises(KeyError): _ = graph.GetVertexByValue(1) vertex = Vertex(value=1, graph=graph) self.assertIs(vertex, graph.GetVertexByValue(1)) vertex = Vertex(value=1, graph=graph) with self.assertRaises(KeyError): _ = graph.GetVertexByValue(1) def test_EdgeNoneValue(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) edge12 = vertex1.EdgeToVertex(vertex2) self.assertIsNone(edge12.Value) edge12.Value = 5 self.assertEqual(5, edge12.Value) def test_EdgeValue(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) edge12 = vertex1.EdgeToVertex(vertex2, edgeValue=3) self.assertEqual(3, edge12.Value) edge12.Value = None self.assertIsNone(edge12.Value) class Weights(TestCase): def test_VertexNoneWeight(self) -> None: graph = Graph() vertex = Vertex(graph=graph) self.assertIsNone(vertex.Weight) vertex.Weight = 5 self.assertEqual(5, vertex.Weight) def test_VertexWeight(self) -> None: graph = Graph() vertex = Vertex(weight=1, graph=graph) self.assertEqual(1, vertex.Weight) vertex.Weight = None self.assertIsNone(vertex.Weight) def test_EdgeNoneWeight(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) edge12 = vertex1.EdgeToVertex(vertex2) self.assertIsNone(edge12.Weight) edge12.Weight = 5 self.assertEqual(5, edge12.Weight) def test_EdgeWeight(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) edge12 = vertex1.EdgeToVertex(vertex2, edgeWeight=3) self.assertEqual(3, edge12.Weight) edge12.Weight = None self.assertIsNone(edge12.Weight) class Dicts(TestCase): def test_GraphDict(self) -> None: graph = Graph() self.assertEqual(0, len(graph)) graph["key"] = 2 self.assertEqual(2, graph["key"]) self.assertEqual(1, len(graph)) self.assertIn("key", graph) del graph["key"] self.assertNotIn("key", graph) self.assertEqual(0, len(graph)) with self.assertRaises(KeyError): _ = graph["key"] def test_SubgraphDict(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) self.assertEqual(0, len(subgraph)) subgraph["key"] = 2 self.assertEqual(2, subgraph["key"]) self.assertEqual(1, len(subgraph)) self.assertIn("key", subgraph) del subgraph["key"] self.assertNotIn("key", subgraph) self.assertEqual(0, len(subgraph)) with self.assertRaises(KeyError): _ = subgraph["key"] def test_ViewDict(self) -> None: graph = Graph() view = View(graph=graph) self.assertEqual(0, len(view)) view["key"] = 2 self.assertEqual(2, view["key"]) self.assertEqual(1, len(view)) self.assertIn("key", view) del view["key"] self.assertNotIn("key", view) self.assertEqual(0, len(view)) with self.assertRaises(KeyError): _ = view["key"] def test_ComponentDict(self) -> None: graph = Graph() component = Vertex(graph=graph).Component self.assertEqual(0, len(component)) component["key"] = 2 self.assertEqual(2, component["key"]) self.assertEqual(1, len(component)) self.assertIn("key", component) del component["key"] self.assertNotIn("key", component) self.assertEqual(0, len(component)) with self.assertRaises(KeyError): _ = component["key"] def test_VertexDict(self) -> None: graph = Graph() vertex = Vertex(graph=graph) self.assertEqual(0, len(vertex)) vertex["key"] = 2 self.assertEqual(2, vertex["key"]) self.assertEqual(1, len(vertex)) self.assertIn("key", vertex) del vertex["key"] self.assertNotIn("key", vertex) self.assertEqual(0, len(vertex)) with self.assertRaises(KeyError): _ = vertex["key"] def test_EdgeDict(self) -> None: graph = Graph() vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) edge12 = vertex1.EdgeToVertex(vertex2) self.assertEqual(0, len(edge12)) edge12["key"] = 2 self.assertEqual(2, edge12["key"]) self.assertEqual(1, len(edge12)) self.assertIn("key", edge12) del edge12["key"] self.assertNotIn("key", edge12) self.assertEqual(0, len(edge12)) with self.assertRaises(KeyError): _ = edge12["key"] class EdgesAndLinks(TestCase): def test_EdgeToVertex(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) edge12 = vertex1.EdgeToVertex(vertex2) edge34 = vertex3.EdgeToVertex(vertex4) self.assertEqual(vertex1, edge12.Source) self.assertEqual(vertex2, edge12.Destination) self.assertEqual(vertex3, edge34.Source) self.assertEqual(vertex4, edge34.Destination) def test_EdgeToVertexWithID(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) edge12 = vertex1.EdgeToVertex(vertex2, edgeID=1) edge34 = vertex3.EdgeToVertex(vertex4, edgeID=2) self.assertEqual(vertex1, edge12.Source) self.assertEqual(vertex2, edge12.Destination) self.assertEqual(1, edge12.ID) self.assertEqual(vertex3, edge34.Source) self.assertEqual(vertex4, edge34.Destination) self.assertEqual(2, edge34.ID) def test_DuplicateEdgeID(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) vertex1.EdgeToVertex(vertex2, edgeID=1) vertex3.EdgeToVertex(vertex4, edgeID=1) with self.assertRaises(DuplicateEdgeError): vertex2.EdgeToVertex(vertex1, edgeID=1) with self.assertRaises(DuplicateEdgeError): vertex4.EdgeToVertex(vertex3, edgeID=1) with self.assertRaises(DuplicateEdgeError): vertex1.EdgeFromVertex(vertex2, edgeID=1) with self.assertRaises(DuplicateEdgeError): vertex3.EdgeFromVertex(vertex4, edgeID=1) def test_EdgeToVertexWithValue(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) edge12 = vertex1.EdgeToVertex(vertex2, edgeValue=1) edge34 = vertex3.EdgeToVertex(vertex4, edgeValue=2) self.assertEqual(vertex1, edge12.Source) self.assertEqual(vertex2, edge12.Destination) self.assertEqual(1, edge12.Value) self.assertEqual(vertex3, edge34.Source) self.assertEqual(vertex4, edge34.Destination) self.assertEqual(2, edge34.Value) def test_EdgeToVertexWithWeight(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) edge12 = vertex1.EdgeToVertex(vertex2, edgeWeight=1) edge34 = vertex3.EdgeToVertex(vertex4, edgeWeight=2) self.assertEqual(vertex1, edge12.Source) self.assertEqual(vertex2, edge12.Destination) self.assertEqual(1, edge12.Weight) self.assertEqual(vertex3, edge34.Source) self.assertEqual(vertex4, edge34.Destination) self.assertEqual(2, edge34.Weight) def test_EdgeFromVertex(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) edge12 = vertex2.EdgeFromVertex(vertex1) edge34 = vertex4.EdgeFromVertex(vertex3) self.assertEqual(vertex1, edge12.Source) self.assertEqual(vertex2, edge12.Destination) self.assertEqual(vertex3, edge34.Source) self.assertEqual(vertex4, edge34.Destination) def test_EdgeFromVertexWithID(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) edge12 = vertex2.EdgeFromVertex(vertex1, edgeID=1) edge34 = vertex4.EdgeFromVertex(vertex3, edgeID=2) self.assertEqual(vertex1, edge12.Source) self.assertEqual(vertex2, edge12.Destination) self.assertEqual(1, edge12.ID) self.assertEqual(vertex3, edge34.Source) self.assertEqual(vertex4, edge34.Destination) self.assertEqual(2, edge34.ID) def test_EdgeFromVertexWithValue(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) edge12 = vertex2.EdgeFromVertex(vertex1, edgeValue=1) edge34 = vertex4.EdgeFromVertex(vertex3, edgeValue=2) self.assertEqual(vertex1, edge12.Source) self.assertEqual(vertex2, edge12.Destination) self.assertEqual(1, edge12.Value) self.assertEqual(vertex3, edge34.Source) self.assertEqual(vertex4, edge34.Destination) self.assertEqual(2, edge34.Value) def test_EdgeFromVertexWithWeight(self) -> None: graph = Graph() subgraph = Subgraph(graph=graph) vertex1 = Vertex(graph=graph) vertex2 = Vertex(graph=graph) vertex3 = Vertex(subgraph=subgraph) vertex4 = Vertex(subgraph=subgraph) edge12 = vertex2.EdgeFromVertex(vertex1, edgeWeight=1) edge34 = vertex4.EdgeFromVertex(vertex3, edgeWeight=2) self.assertEqual(vertex1, edge12.Source) self.assertEqual(vertex2, edge12.Destination) self.assertEqual(1, edge12.Weight) self.assertEqual(vertex3, edge34.Source) self.assertEqual(vertex4, edge34.Destination) self.assertEqual(2, edge34.Weight) class Iterate(TestCase): class TestGraph: _vertexCount: int _edgeCount: int _edges: List[Tuple[int, int, int]] def __init__(self, edges: List[Tuple[int, int, int]]) -> None: self._vertexCount = max([max(e[0], e[1]) for e in edges]) + 1 self._edgeCount = len(edges) self._edges = edges @readonly def VertexCount(self) -> int: return self._vertexCount @readonly def EdgeCount(self) -> int: return self._edgeCount @readonly def Edges(self) -> List[Tuple[int, int, int]]: return self._edges _graph0 = TestGraph([ (0, 3, 1), (1, 3, 2), (2, 0, 3), (2, 1, 4), # root (3, 6, 5), (3, 7, 6), (4, 0, 7), (4, 3, 8), (4, 5, 9), # root (5, 9, 10), (5, 10, 11), (6, 8, 12), (7, 8, 13), (7, 9, 14), (8, 11, 15), (9, 11, 16), (9, 12, 17), (10, 9, 18), # 11 leaf # 12 leaf (13, 14, 19), # root # 14 leaf ]) _graph1 = TestGraph([ (0, 1, 0), (0, 9, 0), (1, 8, 0), (2, 8, 0), (3, 2, 0), (3, 4, 0), (3, 5, 0), (6, 5, 0), (7, 3, 0), (7, 6, 0), (7, 10, 0), (7, 11, 0), (8, 7, 0), (9, 1, 0), (9, 8, 0), (10, 9, 0), (10, 11, 0), (11, 7, 0), (12, 2, 0), (12, 8, 0), (13, 14, 0), ]) _graph2 = TestGraph([ (0, 1, 1), (0, 2, 2), (0, 3, 3), (1, 2, 3), (1, 10, 3), (2, 7, 20), (2, 8, 6), (3, 2, 1), (3, 4, 1), (4, 5, 1), (4, 7, 1), (5, 6, 1), (5, 7, 1), (6, 0, 4), (6, 3, 2), (6, 7, 5), (6, 13, 8), (7, 11, 6), (7, 12, 1), (8, 1, 5), (8, 10, 1), (8, 11, 16), (9, 3, 4), (9, 6, 1), (11, 4, 4), (11, 10, 4), (11, 14, 1), (13, 14, 3), (14, 10, 9), ]) _tree0 = TestGraph([ (0, 1, 1), (0, 2, 2), (0, 3, 3), # root (1, 4, 4), (1, 5, 5), # leaf, leaf # 2 (3, 6, 6), (3, 7, 7), (3, 8, 8), # node, leaf, leaf # 4 # 5 # 6 # 7 (8, 9, 9), (9, 10, 10), (10, 11, 11), (10, 12, 12), (10, 13, 13), # leaf, leaf, leaf ]) class IterateOnGraph(Iterate): def test_Roots(self) -> None: g = Graph() vList = [Vertex(value=i, graph=g) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) self.assertSetEqual({2, 4, 13}, set(v.Value for v in g.IterateRoots())) self.assertSetEqual({2, 4}, set(v.Value for v in g.IterateRoots(predicate=lambda v: v.Value % 2 == 0))) def test_Leafs(self) -> None: g = Graph() vList = [Vertex(value=i, graph=g) if i % 2 == 0 else Vertex(vertexID=i, value=i, graph=g) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) self.assertSetEqual({11, 12, 14}, set(v.Value for v in g.IterateLeafs())) self.assertSetEqual({12, 14}, set(v.Value for v in g.IterateLeafs(predicate=lambda v: v.Value % 2 == 0))) def test_Vertices(self) -> None: gID = Graph() gValue = Graph() gMixed = Graph() vListID = [Vertex(vertexID=i, value=i, graph=gID) for i in range(0, self._graph0.VertexCount)] vListValue = [Vertex(value=i, graph=gValue) for i in range(0, self._graph0.VertexCount)] vListMixed = [Vertex(value=i, graph=gMixed) if i % 2 == 0 else Vertex(vertexID=i, value=i, graph=gMixed) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: vListID[u].EdgeToVertex(vListID[v], edgeWeight=w) vListValue[u].EdgeToVertex(vListValue[v], edgeWeight=w) vListMixed[u].EdgeToVertex(vListMixed[v], edgeWeight=w) self.assertListEqual([i for i in range(0, 15, 1)], [v.Value for v in gID.IterateVertices()]) self.assertListEqual([i for i in range(0, 15, 2)], [v.Value for v in gID.IterateVertices(predicate=lambda v: v.Value % 2 == 0)]) self.assertListEqual([i for i in range(0, 15, 1)], [v.Value for v in gValue.IterateVertices()]) self.assertListEqual([i for i in range(0, 15, 2)], [v.Value for v in gValue.IterateVertices(predicate=lambda v: v.Value % 2 == 0)]) self.assertListEqual([i for i in range(0, 15, 2)] + [i for i in range(1, 15, 2)], [v.Value for v in gMixed.IterateVertices()]) self.assertListEqual([i for i in range(0, 15, 2)], [v.Value for v in gMixed.IterateVertices(predicate=lambda v: v.Value % 2 == 0)]) def test_Topologically(self) -> None: g = Graph() vList = [Vertex(value=i, graph=g) if i % 2 == 0 else Vertex(vertexID=i, value=i, graph=g) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) self.assertListEqual([12, 14, 11, 13, 8, 9, 6, 7, 10, 3, 5, 0, 1, 4, 2], [v.Value for v in g.IterateTopologically()]) self.assertListEqual([12, 14, 8, 6, 10, 0, 4, 2], [v.Value for v in g.IterateTopologically(predicate=lambda v: v.Value % 2 == 0)]) self.assertListEqual([11, 13, 9, 7, 3, 5, 1], [v.Value for v in g.IterateTopologically(predicate=lambda v: v.Value % 2 == 1)]) def test_Edges(self) -> None: g = Graph() vList = [Vertex(value=i, graph=g) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) vList[v].EdgeToVertex(vList[u], edgeWeight=self._graph0.EdgeCount + w, edgeID=v * 20 + u) for i, edge in enumerate(g.IterateEdges()): if i < self._graph0.EdgeCount: u, v, w = self._graph0.Edges[i % self._graph0.EdgeCount] else: v, u, w = self._graph0.Edges[i % self._graph0.EdgeCount] w += self._graph0.EdgeCount self.assertEqual(vList[u], edge.Source) self.assertEqual(vList[v], edge.Destination) self.assertEqual(w, edge.Weight) for i, edge in enumerate(g.IterateEdges(predicate=lambda v: v.Weight % 2 == 0), start=1): self.assertEqual(i * 2, edge.Weight) class GraphOperations(Iterate): def test_ReverseEdges(self) -> None: g = Graph() vList = [Vertex(value=i, graph=g) if i % 2 == 0 else Vertex(vertexID=i, value=i, graph=g) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: if w < (self._graph0.EdgeCount // 2): vList[u].EdgeToVertex(vList[v], edgeWeight=w) else: vList[u].EdgeToVertex(vList[v], edgeWeight=w, edgeID=w) g.ReverseEdges() for i, edge in enumerate(g.IterateEdges()): u, v, w = self._graph0.Edges[i] self.assertEqual((vList[v], vList[u], w), (edge.Source, edge.Destination, edge.Weight)) g.ReverseEdges(predicate=lambda e: e.Weight % 2 == 1) for i, edge in enumerate(g.IterateEdges()): u, v, w = self._graph0.Edges[i] if w % 2 == 0: v, u = u, v self.assertEqual((vList[u], vList[v], w), (edge.Source, edge.Destination, edge.Weight)) def test_RemoveEdges(self) -> None: g = Graph() vList = [Vertex(value=i, graph=g) if i % 2 == 0 else Vertex(vertexID=i, value=i, graph=g) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: if w < (self._graph0.EdgeCount // 2): vList[u].EdgeToVertex(vList[v], edgeWeight=w) else: vList[u].EdgeToVertex(vList[v], edgeWeight=w, edgeID=w) g.RemoveEdges(predicate=lambda e: e.Weight % 2 == 0) for i, edge in enumerate(g.IterateEdges()): u, v, w = self._graph0.Edges[i*2] self.assertEqual(w, edge.Weight) self.assertTrue(edge.Weight % 2 == 1) g.RemoveEdges() self.assertEqual(0, g.EdgeCount) def test_CopyVertices(self) -> None: g0 = Graph() vList = [Vertex(value=i, graph=g0) if i % 2 == 0 else Vertex(vertexID=i, value=i, graph=g0) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: if w < (self._graph0.EdgeCount // 2): vList[u].EdgeToVertex(vList[v], edgeWeight=w) else: vList[u].EdgeToVertex(vList[v], edgeWeight=w, edgeID=w) g1 = g0.CopyVertices() self.assertEqual(len(g0), len(g1)) for v0, v1 in zip(g0.IterateVertices(), g1.IterateVertices()): self.assertTupleEqual((v0.ID, v0.Value, len(v0)), (v1.ID, v1.Value, len(v1))) g2 = g0.CopyVertices(copyGraphDict=False, copyVertexDict=False) self.assertEqual(0, len(g2)) for v0, v2 in zip(g0.IterateVertices(), g2.IterateVertices()): self.assertTupleEqual((v0.ID, v0.Value, 0), (v2.ID, v2.Value, len(v2))) g3 = g0.CopyVertices(predicate=lambda e: e.Value % 2 == 0) self.assertEqual(len(g0), len(g3)) for v3 in g3.IterateVertices(): self.assertTrue(v3.Value % 2 == 0) # self.assertEqual(len(v0), len(v3)) g4 = g0.CopyVertices(predicate=lambda e: e.Value % 2 == 1, copyGraphDict=False, copyVertexDict=False) self.assertEqual(0, len(g4)) for v4 in g4.IterateVertices(): self.assertTrue(v4.Value % 2 == 1) self.assertEqual(0, len(v4)) class VertexOperations(Iterate): def test_CopyIntoSameGraph(self) -> None: graph1 = Graph() vertex1 = Vertex(graph=graph1) with self.assertRaises(GraphException): vertex1.Copy(graph1) def test_Copy(self) -> None: graph1 = Graph() graph2 = Graph() vertex1 = Vertex(graph=graph1) vertex1["key"] = "value" vertex2 = vertex1.Copy(graph2) self.assertEqual(1, graph2.VertexCount) self.assertEqual(0, len(vertex2)) def test_CopyWithDict(self) -> None: graph1 = Graph() graph2 = Graph() vertex1 = Vertex(graph=graph1) vertex1["key"] = "value" vertex2 = vertex1.Copy(graph2, copyDict=True) self.assertEqual(1, len(vertex2)) self.assertIn("key", vertex2) self.assertEqual("value", vertex2["key"]) def test_CopyAddForwardLink(self) -> None: graph1 = Graph() graph2 = Graph() vertex1 = Vertex(graph=graph1) vertex1["key"] = "value" vertex2 = vertex1.Copy(graph2, linkingKeyFromOriginalVertex="forward") self.assertEqual(2, len(vertex1)) self.assertEqual(0, len(vertex2)) self.assertIn("forward", vertex1) self.assertNotIn("forward", vertex2) self.assertEqual(vertex2, vertex1["forward"]) def test_CopyAddBackwardLink(self) -> None: graph1 = Graph() graph2 = Graph() vertex1 = Vertex(graph=graph1) vertex1["key"] = "value" vertex2 = vertex1.Copy(graph2, linkingKeyToOriginalVertex="backward") self.assertEqual(1, len(vertex1)) self.assertEqual(1, len(vertex2)) self.assertNotIn("backward", vertex1) self.assertIn("backward", vertex2) self.assertEqual(vertex1, vertex2["backward"]) class GraphProperties(Iterate): def test_HasCycle(self) -> None: g0 = Graph() vList0 = [Vertex(value=i, graph=g0) if i % 2 == 0 else Vertex(vertexID=i, value=i, graph=g0) for i in range(0, self._graph0.VertexCount)] for u, v, w in self._graph0.Edges: vList0[u].EdgeToVertex(vList0[v], edgeWeight=w) self.assertFalse(g0.HasCycle()) g1 = Graph() vList1 = [Vertex(vertexID=i, graph=g1) for i in range(0, self._graph1.VertexCount)] for u, v, w in self._graph1.Edges: vList1[u].EdgeToVertex(vList1[v], edgeWeight=w) self.assertTrue(g1.HasCycle()) class IterateStartingFromVertex(Iterate): def test_DFS(self) -> None: g = Graph() vList = [Vertex(vertexID=i, graph=g) for i in range(0, self._graph1.VertexCount)] v0 = vList[0] for u, v, w in self._graph1.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) self.assertListEqual([0, 1, 8, 7, 3, 2, 4, 5, 6, 10, 9, 11], [v.ID for v in v0.IterateVerticesDFS()]) def test_BFS(self) -> None: g = Graph() vList = [Vertex(vertexID=i, graph=g) for i in range(0, self._graph1.VertexCount)] v0 = vList[0] for u, v, w in self._graph1.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) self.assertListEqual([0, 1, 9, 8, 7, 3, 6, 10, 11, 2, 4, 5], [v.ID for v in v0.IterateVerticesBFS()]) def test_IterateAllOutboundPathsAsVertexList(self) -> None: g = Graph() vList = [Vertex(vertexID=i, graph=g) for i in range(0, self._graph2.VertexCount)] v0 = vList[0] for u, v, w in self._graph2.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) source = vList[10] paths = [path for path in source.IterateAllOutboundPathsAsVertexList()] self.assertEqual(1, len(paths)) self.assertTupleEqual((source,), paths[0]) source = vList[7] with self.assertRaises(CycleError): for path in source.IterateAllOutboundPathsAsVertexList(): pass # Removing a reverse edge to avoid cycles. vList[11].DeleteEdgeTo(vList[4]) source = vList[7] expectedPaths = ( (source, vList[11], vList[10]), (source, vList[11], vList[14], vList[10]), (source, vList[12]), ) for i, path in enumerate(source.IterateAllOutboundPathsAsVertexList()): print(f"{i}: {path}") self.assertTupleEqual(expectedPaths[i], path) def test_ShortestPathByHops(self) -> None: g = Graph() vList = [Vertex(vertexID=i, graph=g) for i in range(0, self._graph2.VertexCount)] v0 = vList[0] for u, v, w in self._graph2.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) self.assertListEqual([0, 2, 7, 11, 14], [v.ID for v in v0.ShortestPathToByHops(vList[14])]) with self.assertRaises(DestinationNotReachable): print([v.ID for v in v0.ShortestPathToByHops(vList[9])]) def test_ShortestPathByFixedWeight(self) -> None: g = Graph() vList = [Vertex(vertexID=i, graph=g) for i in range(0, self._graph2.VertexCount)] v0 = vList[0] for u, v, _ in self._graph2.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=1) self.assertListEqual([0, 2, 7, 11, 14], [v.ID for v, w in v0.ShortestPathToByWeight(vList[14])]) with self.assertRaises(DestinationNotReachable): print([v.ID for v in v0.ShortestPathToByHops(vList[9])]) def test_ShortestPathByWeight(self) -> None: g = Graph() vList = [Vertex(vertexID=i, graph=g) for i in range(0, self._graph2.VertexCount)] v0 = vList[0] for u, v, w in self._graph2.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) self.assertListEqual([0, 3, 4, 5, 6, 13, 14], [v.ID for v, w in v0.ShortestPathToByWeight(vList[14])]) with self.assertRaises(DestinationNotReachable): print([v.ID for v in v0.ShortestPathToByHops(vList[9])]) class GraphToTree(Iterate): def test_ConvertToTree(self) -> None: g = Graph() vList = [Vertex(value=i, graph=g) for i in range(0, self._tree0.VertexCount)] for u, v, w in self._tree0.Edges: vList[u].EdgeToVertex(vList[v], edgeWeight=w) root = vList[0] tree = root.ConvertToTree() self.assertEqual(g.VertexCount, tree.Size) self.assertSetEqual(set([v.Value for v in g.IterateLeafs()]), set([n.Value for n in tree.IterateLeafs()])) pyTooling-8.11.0/tests/unit/Licensing/000077500000000000000000000000001513317154500176265ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Licensing/Licensing.py000066400000000000000000000133321513317154500221150ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_) ___ ___ _ __ ___(_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ __/ _ \ '_ \/ __| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | (_| __/ | | \__ \ | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|\___\___|_| |_|___/_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # from unittest import TestCase from pyTooling.Licensing import PYTHON_LICENSE_NAMES, SPDX_INDEX, License if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class LicenseDataClass(TestCase): def test_Properies(self) -> None: license = License("spdx", "License Name", False, False) self.assertEqual("spdx", license.SPDXIdentifier) self.assertEqual("License Name", license.Name) self.assertEqual(False, license.OSIApproved) self.assertEqual(False, license.FSFApproved) def test_ClassifierConversion(self) -> None: license = License("Apache-2.0", "License Name", True, False) self.assertEqual("License :: OSI Approved :: Apache Software License", license.PythonClassifier) def test_ClassifierConversionException(self) -> None: license = License("spdx", "License Name", False, False) with self.assertRaises(ValueError): _ = license.PythonClassifier def test_Equalality(self) -> None: license1 = License("spdx", "License Name", False, False) license2 = License("spdx", "License Name", False, False) license3 = License("SPDX", "License Name", False, False) self.assertTrue(license1 == license2) self.assertTrue(license1 != license3) with self.assertRaises(TypeError): _ = license1 == "spdx" with self.assertRaises(TypeError): _ = license1 != "spdx" def test_Compatibility(self) -> None: license1 = License("spdx", "License Name", False, False) license2 = License("spdx", "License Name", False, False) with self.assertRaises(NotImplementedError): _ = license1 <= license2 with self.assertRaises(NotImplementedError): _ = license1 >= license2 def test_ToString(self) -> None: license = License("spdx", "License Name", False, False) self.assertEqual("spdx", f"{license!r}") self.assertEqual("License Name", f"{license!s}") class SPDXLicenses(TestCase): def test_Apache(self) -> None: self.assertIn("Apache-2.0", SPDX_INDEX) self.assertIn("Apache-2.0", PYTHON_LICENSE_NAMES) # class PythonClassifiers(TestCase): # def test_OSIApproved(self) -> None: # for spdxId, item in PYTHON_LICENSE_NAMES.items(): # license = SPDX_INDEX[spdxId] # self.assertEqual("OSI Approved" in item.Classifier, license.OSIApproved) pyTooling-8.11.0/tests/unit/LinkedList/000077500000000000000000000000001513317154500177555ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/LinkedList/__init__.py000066400000000000000000000707061513317154500221000ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _ _ _ _ _ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_)_ __ | | _____ __| | | (_)___| |_ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | | '_ \| |/ / _ \/ _` | | | / __| __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | | | | < __/ (_| | |___| \__ \ |_ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|_| |_|_|\_\___|\__,_|_____|_|___/\__| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for pyTooling.LinkedList.""" from unittest import TestCase from pyTooling.LinkedList import Node, LinkedList, LinkedListException if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Node(self) -> None: node = Node(5) self.assertEqual(5, node.Value) self.assertIsNone(node.List) self.assertIsNone(node.PreviousNode) self.assertIsNone(node.NextNode) def test_Node_Previous(self) -> None: previous = Node(4) node = Node(5, previousNode=previous) self.assertEqual(4, previous.Value) self.assertEqual(5, node.Value) self.assertIsNone(previous.List) self.assertIsNone(node.List) self.assertIs(previous, node.PreviousNode) self.assertIs(node, previous.NextNode) self.assertIsNone(previous.PreviousNode) self.assertIsNone(node.NextNode) def test_Node_Previous_WrongType(self) -> None: with self.assertRaises(TypeError): _ = Node(5, previousNode=4) def test_Node_Next(self) -> None: next = Node(6) node = Node(5, nextNode=next) self.assertEqual(5, node.Value) self.assertEqual(6, next.Value) self.assertIsNone(node.List) self.assertIsNone(next.List) self.assertIs(next, node.NextNode) self.assertIs(node, next.PreviousNode) self.assertIsNone(next.NextNode) self.assertIsNone(node.PreviousNode) def test_Node_Next_WrongType(self) -> None: with self.assertRaises(TypeError): _ = Node(5, nextNode=6) def test_LinkedList(self) -> None: linkedList = LinkedList() self.assertEqual(0, len(linkedList)) self.assertEqual(0, linkedList.Count) self.assertIsNone(linkedList.FirstNode) self.assertIsNone(linkedList.LastNode) self.assertTrue(linkedList.IsEmpty) def test_LinkedList_EmptyTuple(self) -> None: linkedList = LinkedList(tuple()) self.assertEqual(0, linkedList.Count) def test_LinkedList_EmptyGenerator(self) -> None: nodes = tuple() linkedList = LinkedList(n for n in nodes) self.assertEqual(0, linkedList.Count) def test_LinkedList_WrongType(self) -> None: with self.assertRaises(TypeError): _ = LinkedList(42) def test_LinkedList_Tuple1(self) -> None: nodes = (Node(0), ) linkedList = LinkedList(nodes) self.assertEqual(1, linkedList.Count) def test_LinkedList_Tuple1_WrongType(self) -> None: nodes = (0, ) with self.assertRaises(TypeError): _ = LinkedList(nodes) def test_LinkedList_Tuple1_UsedNode(self) -> None: node0 = Node(0) node0._linkedList = "list" nodes = (node0, ) with self.assertRaises(LinkedListException): _ = LinkedList(nodes) def test_LinkedList_Tuple2(self) -> None: nodes = (Node(0), Node(1)) linkedList = LinkedList(nodes) self.assertEqual(2, linkedList.Count) def test_LinkedList_Tuple2_WrongType(self) -> None: nodes = (Node(0), 1) with self.assertRaises(TypeError): _ = LinkedList(nodes) def test_LinkedList_Tuple2_UsedNode(self) -> None: node0 = Node(0) node1 = Node(1) node1._linkedList = "list" nodes = (node0, node1) with self.assertRaises(LinkedListException): _ = LinkedList(nodes) def test_LinkedList_Tuple3(self) -> None: nodes = (Node(0), Node(1), Node(2)) linkedList = LinkedList(nodes) self.assertEqual(3, linkedList.Count) class Properties(TestCase): def test_Count(self) -> None: ll = LinkedList() self.assertEqual(0, ll.Count) self.assertEqual(0, len(ll)) ll.InsertAfterLast(Node(0)) self.assertEqual(1, ll.Count) self.assertEqual(1, len(ll)) def test_Value(self) -> None: node = Node(5) self.assertEqual(5, node.Value) node.Value = "5" self.assertEqual("5", node.Value) class Insert(TestCase): def test_InsertFirst(self) -> None: ll = LinkedList() node = Node(0) ll.InsertBeforeFirst(node) self.assertEqual(1, ll.Count) self.assertIs(node, ll.FirstNode) self.assertIs(node, ll.LastNode) self.assertIs(ll, node.List) self.assertIsNone(node.PreviousNode) self.assertIsNone(node.NextNode) def test_InsertFirst2(self) -> None: ll = LinkedList() node1 = Node(1) node2 = Node(2) ll.InsertBeforeFirst(node2) ll.InsertBeforeFirst(node1) self.assertEqual(2, ll.Count) self.assertIs(node1, ll.FirstNode) self.assertIs(node2, ll.LastNode) self.assertIs(ll, node1.List) self.assertIs(ll, node2.List) self.assertIsNone(node1.PreviousNode) self.assertIs(node1, node2.PreviousNode) self.assertIs(node2, node1.NextNode) self.assertIsNone(node2.NextNode) def test_InsertFirst_None(self) -> None: ll = LinkedList() with self.assertRaises(ValueError): ll.InsertBeforeFirst(None) def test_InsertFirst_WrongType(self) -> None: ll = LinkedList() with self.assertRaises(TypeError): ll.InsertBeforeFirst("0") def test_InsertFirst_UsedNode(self) -> None: ll = LinkedList() node = Node(0) node._linkedList = "list" with self.assertRaises(LinkedListException): ll.InsertBeforeFirst(node) def test_InsertLast(self) -> None: ll = LinkedList() node = Node(0) ll.InsertAfterLast(node) self.assertEqual(1, ll.Count) self.assertIs(node, ll.FirstNode) self.assertIs(node, ll.LastNode) self.assertIs(ll, node.List) self.assertIsNone(node.PreviousNode) self.assertIsNone(node.NextNode) def test_InsertLast2(self) -> None: ll = LinkedList() node1 = Node(1) node2 = Node(2) ll.InsertAfterLast(node1) ll.InsertAfterLast(node2) self.assertEqual(2, ll.Count) self.assertIs(node1, ll.FirstNode) self.assertIs(node2, ll.LastNode) self.assertIs(ll, node1.List) self.assertIs(ll, node2.List) self.assertIsNone(node1.PreviousNode) self.assertIs(node1, node2.PreviousNode) self.assertIs(node2, node1.NextNode) self.assertIsNone(node2.NextNode) def test_InsertLast_None(self) -> None: ll = LinkedList() with self.assertRaises(ValueError): ll.InsertAfterLast(None) def test_InsertLast_WrongType(self) -> None: ll = LinkedList() with self.assertRaises(TypeError): ll.InsertAfterLast("0") def test_InsertLast_UsedNode(self) -> None: ll = LinkedList() node = Node(0) node._linkedList = "list" with self.assertRaises(LinkedListException): ll.InsertAfterLast(node) def test_InserBefore(self) -> None: ll = LinkedList() node0 = Node(0) node2 = Node(2) ll.InsertAfterLast(node0) ll.InsertAfterLast(node2) node1 = Node(1) node2.InsertNodeBefore(node1) self.assertEqual(3, ll.Count) self.assertIs(node0, ll.FirstNode) self.assertIs(node2, ll.LastNode) self.assertIsNone(node0.PreviousNode) self.assertIs(node1, node0.NextNode) self.assertIs(node0, node1.PreviousNode) self.assertIs(node2, node1.NextNode) self.assertIs(node1, node2.PreviousNode) self.assertIsNone(node2.NextNode) def test_InserBefore_First(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) node0 = Node(0) node1.InsertNodeBefore(node0) self.assertEqual(2, ll.Count) self.assertIs(node0, ll.FirstNode) self.assertIs(node1, ll.LastNode) self.assertIsNone(node0.PreviousNode) self.assertIs(node1, node0.NextNode) self.assertIs(node0, node1.PreviousNode) def test_InserBefore_None(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) with self.assertRaises(ValueError): node1.InsertNodeBefore(None) def test_InserBefore_WrongType(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) with self.assertRaises(TypeError): node1.InsertNodeBefore("node") def test_InserBefore_UsedNode(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) node0 = Node(0) node0._linkedList = "list" with self.assertRaises(LinkedListException): node1.InsertNodeBefore(node0) def test_InserAfter(self) -> None: ll = LinkedList() node0 = Node(0) node2 = Node(2) ll.InsertAfterLast(node0) ll.InsertAfterLast(node2) node1 = Node(1) node0.InsertNodeAfter(node1) self.assertEqual(3, ll.Count) self.assertIs(node0, ll.FirstNode) self.assertIs(node2, ll.LastNode) self.assertIsNone(node0.PreviousNode) self.assertIs(node1, node0.NextNode) self.assertIs(node0, node1.PreviousNode) self.assertIs(node2, node1.NextNode) self.assertIs(node1, node2.PreviousNode) self.assertIsNone(node2.NextNode) def test_InserAfter_First(self) -> None: ll = LinkedList() node0 = Node(0) ll.InsertAfterLast(node0) node1 = Node(1) node0.InsertNodeAfter(node1) self.assertEqual(2, ll.Count) self.assertIs(node0, ll.FirstNode) self.assertIs(node1, ll.LastNode) self.assertIsNone(node0.PreviousNode) self.assertIs(node1, node0.NextNode) self.assertIs(node0, node1.PreviousNode) def test_InserAfter_None(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) with self.assertRaises(ValueError): node1.InsertNodeAfter(None) def test_InserAfter_WrongType(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) with self.assertRaises(TypeError): node1.InsertNodeAfter("node") def test_InserAfter_UsedNode(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) node0 = Node(0) node0._linkedList = "list" with self.assertRaises(LinkedListException): node1.InsertNodeAfter(node0) class Remove(TestCase): def test_RemoveFirst_EmptyList(self) -> None: ll = LinkedList() with self.assertRaises(LinkedListException): ll.RemoveFirst() def test_RemoveFirst(self) -> None: ll = LinkedList() node = Node(0) ll.InsertAfterLast(node) self.assertEqual(1, ll.Count) n = ll.RemoveFirst() self.assertEqual(0, ll.Count) self.assertIsNone(ll.FirstNode) self.assertIsNone(ll.LastNode) self.assertIs(node, n) self.assertEqual(0, n.Value) self.assertIsNone(n.PreviousNode) self.assertIsNone(n.NextNode) self.assertIsNone(n.List) def test_RemoveFirst2(self) -> None: ll = LinkedList() node1 = Node(1) node2 = Node(2) ll.InsertAfterLast(node1) ll.InsertAfterLast(node2) self.assertEqual(2, ll.Count) n = ll.RemoveFirst() self.assertEqual(1, ll.Count) self.assertIs(node2, ll.FirstNode) self.assertIs(node2, ll.LastNode) self.assertIsNone(node2.PreviousNode) self.assertIsNone(node2.NextNode) self.assertIs(node1, n) self.assertEqual(1, n.Value) self.assertIsNone(n.PreviousNode) self.assertIsNone(n.NextNode) self.assertIsNone(n.List) def test_RemoveLast_EmptyList(self) -> None: ll = LinkedList() with self.assertRaises(LinkedListException): ll.RemoveLast() def test_RemoveLast(self) -> None: ll = LinkedList() node = Node(0) ll.InsertBeforeFirst(node) self.assertEqual(1, ll.Count) n = ll.RemoveLast() self.assertEqual(0, ll.Count) self.assertIsNone(ll.FirstNode) self.assertIsNone(ll.LastNode) self.assertIs(node, n) self.assertEqual(0, n.Value) self.assertIsNone(n.PreviousNode) self.assertIsNone(n.NextNode) self.assertIsNone(n.List) def test_RemoveLast2(self) -> None: ll = LinkedList() node1 = Node(1) node2 = Node(2) ll.InsertBeforeFirst(node2) ll.InsertBeforeFirst(node1) self.assertEqual(2, ll.Count) n = ll.RemoveLast() self.assertEqual(1, ll.Count) self.assertIs(node1, ll.FirstNode) self.assertIs(node1, ll.LastNode) self.assertIsNone(node1.PreviousNode) self.assertIsNone(node1.NextNode) self.assertIs(node2, n) self.assertEqual(2, n.Value) self.assertIsNone(n.PreviousNode) self.assertIsNone(n.NextNode) self.assertIsNone(n.List) def test_NodeRemove_One(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) self.assertEqual(1, ll.Count) node1.Remove() self.assertEqual(0, ll.Count) self.assertIsNone(ll.FirstNode) self.assertIsNone(ll.LastNode) self.assertIsNone(node1.PreviousNode) self.assertIsNone(node1.NextNode) def test_NodeRemove_First(self) -> None: ll = LinkedList() node1 = Node(1) node2 = Node(2) ll.InsertAfterLast(node1) ll.InsertAfterLast(node2) self.assertEqual(2, ll.Count) node1.Remove() self.assertEqual(1, ll.Count) self.assertIs(node2, ll.FirstNode) self.assertIs(node2, ll.LastNode) self.assertIsNone(node1.PreviousNode) self.assertIsNone(node1.NextNode) def test_NodeRemove_Middle(self) -> None: ll = LinkedList() node1 = Node(1) node2 = Node(2) node3 = Node(3) ll.InsertAfterLast(node1) ll.InsertAfterLast(node2) ll.InsertAfterLast(node3) self.assertEqual(3, ll.Count) node2.Remove() self.assertEqual(1, node1.Value) self.assertEqual(3, node3.Value) self.assertIs(node1, ll.FirstNode) self.assertIs(node3, ll.LastNode) self.assertIs(ll.FirstNode, ll.LastNode.PreviousNode) self.assertIs(ll.LastNode, ll.FirstNode.NextNode) self.assertIsNone(node2.PreviousNode) self.assertIsNone(node2.NextNode) def test_NodeRemove_Last(self) -> None: ll = LinkedList() node1 = Node(1) node2 = Node(2) ll.InsertBeforeFirst(node2) ll.InsertBeforeFirst(node1) self.assertEqual(2, ll.Count) node2.Remove() self.assertEqual(1, ll.Count) self.assertIs(node1, ll.FirstNode) self.assertIs(node1, ll.LastNode) self.assertIsNone(node2.PreviousNode) self.assertIsNone(node2.NextNode) class MiscOperations(TestCase): def test_Clear_EmptyList(self) -> None: ll = LinkedList() ll.Clear() self.assertEqual(0, ll.Count) self.assertIsNone(ll.FirstNode) self.assertIsNone(ll.LastNode) def test_Clear(self) -> None: ll = LinkedList() ll.InsertBeforeFirst(Node(0)) ll.Clear() self.assertEqual(0, ll.Count) self.assertIsNone(ll.FirstNode) self.assertIsNone(ll.LastNode) def test_Reverse_Empty(self) -> None: ll = LinkedList() ll.Reverse() self.assertEqual(0, ll.Count) self.assertIsNone(ll.FirstNode) self.assertIsNone(ll.LastNode) def test_Reverse1(self) -> None: ll = LinkedList() node = Node(0) ll.InsertAfterLast(node) ll.Reverse() self.assertEqual(1, ll.Count) self.assertIs(ll.FirstNode, ll.LastNode) self.assertIsNone(node.PreviousNode) self.assertIsNone(node.NextNode) def test_Reverse2(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) node2 = Node(2) ll.InsertAfterLast(node2) ll.Reverse() self.assertEqual(2, ll.Count) self.assertIs(node2, ll.FirstNode) self.assertIs(node1, ll.LastNode) self.assertIsNone(node2.PreviousNode) self.assertIsNone(node1.NextNode) def test_Reverse3(self) -> None: ll = LinkedList() node1 = Node(1) ll.InsertAfterLast(node1) node2 = Node(2) ll.InsertAfterLast(node2) node3 = Node(3) ll.InsertAfterLast(node3) ll.Reverse() self.assertEqual(3, ll.Count) self.assertIs(node3, ll.FirstNode) self.assertIs(node1, ll.LastNode) self.assertIsNone(node3.PreviousNode) self.assertIsNone(node1.NextNode) def test_Sort_Empty(self) -> None: ll = LinkedList() ll.Sort() self.assertEqual(0, ll.Count) self.assertIsNone(ll.FirstNode) self.assertIsNone(ll.LastNode) def test_Sort_Single(self) -> None: ll = LinkedList() node0 = Node(0) ll.InsertAfterLast(node0) ll.Sort() self.assertEqual(1, ll.Count) self.assertIs(node0, ll.FirstNode) self.assertIs(node0, ll.LastNode) self.assertIsNone(node0.PreviousNode) self.assertIsNone(node0.NextNode) def test_Sort(self) -> None: sequence = [7, 6, 4, 8, 2, 5, 3, 1, 9] ll = LinkedList() for i in sequence: ll.InsertAfterLast(Node(i)) ll.Sort() self.assertListEqual([i for i in range(1, len(sequence) + 1)], ll.ToList()) def test_Sort_Reverse(self) -> None: sequence = [7, 6, 4, 8, 2, 5, 3, 1, 9] ll = LinkedList() for i in sequence: ll.InsertAfterLast(Node(i)) ll.Sort(reverse=True) self.assertListEqual([i for i in range(len(sequence), 0, -1)], ll.ToList()) def test_Sort_Key(self) -> None: sequence = [7, 6, 4, 8, 2, 5, 3, 1, 9] ll = LinkedList() class Inner: _value: int def __init__(self, value: int): self._value = value for i in sequence: ll.InsertAfterLast(Node(Inner(i))) ll.Sort(key=lambda node: node._value._value) self.assertListEqual([i for i in range(1, len(sequence) + 1)], [n._value for n in ll.ToList()]) class GetNode(TestCase): def test_GetFirst(self) -> None: ll = LinkedList() for i in range(6): ll.InsertAfterLast(Node(i)) first = ll.GetNodeByIndex(0) self.assertEqual(0, first.Value) self.assertIs(ll.FirstNode, first) self.assertIsNone(first.PreviousNode) def test_GetSecond(self) -> None: ll = LinkedList() for i in range(6): ll.InsertAfterLast(Node(i)) second = ll.GetNodeByIndex(1) self.assertEqual(1, second.Value) self.assertIs(ll.FirstNode, second.PreviousNode) self.assertIs(ll.FirstNode.NextNode, second) def test_GetThird(self) -> None: ll = LinkedList() for i in range(6): ll.InsertAfterLast(Node(i)) third = ll.GetNodeByIndex(2) self.assertEqual(2, third.Value) def test_GetLast(self) -> None: ll = LinkedList() for i in range(6): ll.InsertAfterLast(Node(i)) last = ll.GetNodeByIndex(5) self.assertEqual(5, last.Value) self.assertIs(ll.LastNode, last) self.assertIsNone(last.NextNode) def test_Get_Empty(self) -> None: ll = LinkedList() with self.assertRaises(ValueError): _ = ll.GetNodeByIndex(0) def test_Get_OutOfRange(self) -> None: ll = LinkedList() ll.InsertAfterLast(Node(0)) ll.InsertAfterLast(Node(1)) with self.assertRaises(ValueError): _ = ll.GetNodeByIndex(2) class Search(TestCase): def test_Search_Empty(self) -> None: ll = LinkedList() with self.assertRaises(LinkedListException): ll.Search(lambda n: n.Value == 4) def test_Search_NotFound(self) -> None: ll = LinkedList() for i in range(1, 6): ll.InsertAfterLast(Node(i)) with self.assertRaises(LinkedListException): ll.Search(lambda n: n.Value == 10) def test_Search_NotFound_Reverse(self) -> None: ll = LinkedList() for i in range(1, 6): ll.InsertAfterLast(Node(i)) with self.assertRaises(LinkedListException): ll.Search(lambda n: n.Value == 10, reverse=True) def test_Search(self) -> None: ll = LinkedList() for i in range(1, 6): ll.InsertAfterLast(Node(i)) node = ll.Search(lambda n: n.Value % 2 == 0) self.assertEqual(2, node.Value) def test_Search_Reverse(self) -> None: ll = LinkedList() for i in range(1, 6): ll.InsertAfterLast(Node(i)) node = ll.Search(lambda n: n.Value % 2 == 0, reverse=True) self.assertEqual(4, node.Value) class Iterate(TestCase): def test_IterateFromFirst_Empty(self) -> None: ll = LinkedList() with self.assertRaises(StopIteration): iterator = iter(ll.IterateFromFirst()) _ = next(iterator) def test_IterateFromFirst(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) self.assertListEqual(sequence, [n for n in ll.IterateFromFirst()]) def test_IterateFromLast_Empty(self) -> None: ll = LinkedList() with self.assertRaises(StopIteration): iterator = iter(ll.IterateFromLast()) _ = next(iterator) def test_IterateFromLast(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) sequence.reverse() self.assertListEqual(sequence, [n for n in ll.IterateFromLast()]) def test_IterateToFirst_First(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) index = 0 actual = [n for n in sequence[index].IterateToFirst()] self.assertEqual(0, len(actual)) def test_IterateToFirst(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) index = 3 actual = [n for n in sequence[index].IterateToFirst()] self.assertEqual(index, len(actual)) expected = sequence[0:index] expected.reverse() self.assertListEqual(expected, actual) def test_IterateToFirst_Self(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) index = 3 actual = [n for n in sequence[index].IterateToFirst(includeSelf=True)] self.assertEqual(index + 1, len(actual)) expected = sequence[0:index + 1] expected.reverse() self.assertListEqual(expected, actual) def test_IterateToLast_Last(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) index = length - 1 actual = [n for n in sequence[index].IterateToLast()] self.assertEqual(0, len(actual)) def test_IterateToLast(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) index = 1 actual = [n for n in sequence[index].IterateToLast()] self.assertEqual(length - index - 1, len(actual)) expected = sequence[index + 1:length - index + 1] self.assertListEqual(expected, actual) def test_IterateToLast_Self(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) index = 1 actual = [n for n in sequence[index].IterateToLast(includeSelf=True)] self.assertEqual(length - index, len(actual)) expected = sequence[index:length - index + 1] self.assertListEqual(expected, actual) def test_IterateFromFirst_Remove(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) for i, node in enumerate(ll.IterateFromFirst()): value = node.Remove() self.assertIsNone(node._linkedList) self.assertIsNone(node._previousNode) self.assertIsNone(node._nextNode) self.assertEqual(i, value) self.assertEqual(0, ll.Count) def test_IterateFromLast_Remove(self) -> None: ll = LinkedList() length = 5 sequence = [] for i in range(length): node = Node(i) sequence.append(node) ll.InsertAfterLast(node) self.assertEqual(length, ll.Count) for i, node in enumerate(ll.IterateFromLast(), start=1): value = node.Remove() self.assertIsNone(node._linkedList) self.assertIsNone(node._previousNode) self.assertIsNone(node._nextNode) self.assertEqual(length - i, value) self.assertEqual(0, ll.Count) class Conversion(TestCase): def test_ToTuple_Empty(self) -> None: ll = LinkedList() t = ll.ToTuple() self.assertIsInstance(t, tuple) self.assertEqual(0, len(t)) self.assertTupleEqual(tuple(), t) def test_ToTuple(self) -> None: ll = LinkedList() sequence = [] for i in range(5): node = Node(i) sequence.append(i) ll.InsertAfterLast(node) t = ll.ToTuple() self.assertIsInstance(t, tuple) self.assertEqual(len(sequence), len(t)) self.assertTupleEqual(tuple(sequence), t) def test_ToTuple_Reversed(self) -> None: ll = LinkedList() sequence = [] for i in range(5): node = Node(i) sequence.append(i) ll.InsertAfterLast(node) t = ll.ToTuple(reverse=True) sequence.reverse() self.assertIsInstance(t, tuple) self.assertEqual(len(sequence), len(t)) self.assertTupleEqual(tuple(sequence), t) def test_ToList_Empty(self) -> None: ll = LinkedList() l = ll.ToList() self.assertIsInstance(l, list) self.assertEqual(0, len(l)) self.assertListEqual([], l) def test_ToList(self) -> None: ll = LinkedList() sequence = [] for i in range(5): node = Node(i) sequence.append(i) ll.InsertAfterLast(node) l = ll.ToList() self.assertIsInstance(l, list) self.assertEqual(len(sequence), len(l)) self.assertListEqual(sequence, l) def test_ToList_Reversed(self) -> None: ll = LinkedList() sequence = [] for i in range(5): node = Node(i) sequence.append(i) ll.InsertAfterLast(node) l = ll.ToList(reverse=True) sequence.reverse() self.assertIsInstance(l, list) self.assertEqual(len(sequence), len(l)) self.assertListEqual(sequence, l) class Usecases(TestCase): def test_FillBuckets(self) -> None: print() limit = 55 sequence = [ 15, 16, 17, 9, 3, 5, 20, 8, 14, 12, 7, 1, 16, 3, 11, 16, 5, 8, 2, 12, 11, 9, 12, 7, 4, 0, 11, 17, 3, 13, 7, 11, 20, 0, 3, 17, 10, 10, 13, 3, 9, 6, 3, 0, 13, 18, 7, 15, 11, 17 ] ll = LinkedList(Node(i) for i in sequence) index = 0 collected = 0 buckets = [] buckets.append([]) ll.Sort(reverse=True) while True: for node in ll.IterateFromFirst(): if collected + node.Value > limit: continue collected += node.Value buckets[index].append(node.Value) node.Remove() index += 1 if not ll.IsEmpty: collected = 0 buckets.append([]) else: break expected = ((6, 55), (4, 55), (4, 55), (4, 55), (5, 55), (5, 55), (6, 55), (7, 55), (9, 40)) self.assertEqual(len(expected), len(buckets)) for i, bucket in enumerate(buckets): print(f"{i:2}: {len(bucket)} = {sum(bucket)}") self.assertEqual(expected[i][0], len(bucket)) self.assertEqual(expected[i][1], sum(bucket)) pyTooling-8.11.0/tests/unit/MetaClasses/000077500000000000000000000000001513317154500201175ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/MetaClasses/Abstract.py000066400000000000000000000223121513317154500222340ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for class :class:`pyTooling.MetaClasses.ExtendedType`. This test suite tests decorators: * :func:`@abstractmethod ` * :func:`@mustoverride ` """ from unittest import TestCase from pyTooling.Decorators import notimplemented from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride, AbstractClassError if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class AbstractMethod(TestCase): def test_AbstractBase(self) -> None: class AbstractBase(metaclass=ExtendedType): _data: int def __init__(self, data: int) -> None: self._data = data @abstractmethod def AbstractMethod(self) -> bool: return False with self.assertRaises(AbstractClassError) as ExceptionCapture: AbstractBase(1) self.assertIn("AbstractBase", str(ExceptionCapture.exception)) self.assertIn("AbstractMethod", str(ExceptionCapture.exception)) def test_AbstractClass(self) -> None: class AbstractBase(metaclass=ExtendedType): _data: int def __init__(self, data: int) -> None: self._data = data @abstractmethod def AbstractMethod(self) -> bool: return False class AbstractClass(AbstractBase): pass with self.assertRaises(AbstractClassError) as ExceptionCapture: AbstractClass(2) self.assertIn("AbstractClass", str(ExceptionCapture.exception)) self.assertIn("AbstractMethod", str(ExceptionCapture.exception)) def test_DerivedAbstractBase(self) -> None: class AbstractBase(metaclass=ExtendedType): _data: int def __init__(self, data: int) -> None: self._data = data @abstractmethod def AbstractMethod(self) -> bool: return False class DerivedAbstractBase(AbstractBase): def AbstractMethod(self) -> bool: return super().AbstractMethod() derived = DerivedAbstractBase(3) with self.assertRaises(NotImplementedError) as ExceptionCapture: derived.AbstractMethod() self.assertEqual("Method 'AbstractMethod' is abstract and needs to be overridden in a derived class.", str(ExceptionCapture.exception)) def test_DoubleDerivedAbstractBase(self) -> None: class AbstractBase(metaclass=ExtendedType): _data: int def __init__(self, data: int) -> None: self._data = data @abstractmethod def AbstractMethod(self) -> bool: return False class DerivedAbstractBase(AbstractBase): def AbstractMethod(self) -> bool: return super().AbstractMethod() class DoubleDerivedAbstractBase(DerivedAbstractBase): pass derived = DoubleDerivedAbstractBase(4) with self.assertRaises(NotImplementedError) as ExceptionCapture: derived.AbstractMethod() self.assertEqual("Method 'AbstractMethod' is abstract and needs to be overridden in a derived class.", str(ExceptionCapture.exception)) def test_DerivedAbstractClass(self) -> None: class AbstractBase(metaclass=ExtendedType): _data: int def __init__(self, data: int) -> None: self._data = data @abstractmethod def AbstractMethod(self) -> bool: return False class AbstractClass(AbstractBase): pass class DerivedAbstractClass(AbstractClass): def AbstractMethod(self) -> bool: return super().AbstractMethod() derived = DerivedAbstractClass(5) with self.assertRaises(NotImplementedError) as ExceptionCapture: derived.AbstractMethod() self.assertEqual("Method 'AbstractMethod' is abstract and needs to be overridden in a derived class.", str(ExceptionCapture.exception)) def test_MultipleInheritance(self) -> None: class AbstractBase(metaclass=ExtendedType): _data: int def __init__(self, data: int) -> None: self._data = data @abstractmethod def AbstractMethod(self) -> bool: return False class Mixin: def AbstractMethod(self) -> bool: return True class MultipleInheritance(AbstractBase, Mixin): pass derived = MultipleInheritance(6) derived.AbstractMethod() class MustOverride(TestCase): def test_MustOverrideBase(self) -> None: class MustOverrideBase(metaclass=ExtendedType): @mustoverride def MustOverrideMethod(self) -> bool: return False with self.assertRaises(AbstractClassError) as ExceptionCapture: MustOverrideBase() self.assertIn("MustOverrideBase", str(ExceptionCapture.exception)) self.assertIn("MustOverrideMethod", str(ExceptionCapture.exception)) def test_MustOverrideClass(self) -> None: class MustOverrideBase(metaclass=ExtendedType): @mustoverride def MustOverrideMethod(self) -> bool: return False class MustOverrideClass(MustOverrideBase): pass with self.assertRaises(AbstractClassError) as ExceptionCapture: MustOverrideClass() self.assertIn("MustOverrideClass", str(ExceptionCapture.exception)) self.assertIn("MustOverrideMethod", str(ExceptionCapture.exception)) def test_DerivedMustOverride(self) -> None: class MustOverrideBase(metaclass=ExtendedType): @mustoverride def MustOverrideMethod(self) -> bool: return False class DerivedMustOverrideClass(MustOverrideBase): def MustOverrideMethod(self) -> bool: return super().MustOverrideMethod() DerivedMustOverrideClass() class NotImplemented(TestCase): def test_NotImplementedBase(self) -> None: class NotImplementedBase(metaclass=ExtendedType): @notimplemented("It's not working.") def NotYetFinished(self, param: int) -> bool: """Documentation is unfinished.""" return False c = NotImplementedBase() self.assertEqual("Documentation is unfinished.", c.NotYetFinished.__doc__) self.assertEqual("NotYetFinished", c.NotYetFinished.__name__) with self.assertRaises(NotImplementedError) as ExceptionCapture: c.NotYetFinished(4) self.assertEqual("It's not working.", str(ExceptionCapture.exception)) pyTooling-8.11.0/tests/unit/MetaClasses/ClassVariable.py000066400000000000000000000147731513317154500232200ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for class :class:`pyTooling.MetaClasses.ExtendedType`. """ from typing import ClassVar from unittest import TestCase from pyTooling.MetaClasses import ExtendedType, ExtendedTypeError if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class WithoutSlots(TestCase): def test_NoInitValue_NoDunderInit_ClassCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: ClassVar[int] with self.assertRaises(AttributeError, msg="Class field '_data0' shouldn't be initialized on class 'Base'."): _ = Base._data0 def test_NoInitValue_NoDunderInit_InstCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: ClassVar[int] inst = Base() with self.assertRaises(AttributeError, msg="Field '_data0' should not exist on instance."): _ = inst._data0 def test_InitValue_NoDunderInit_ClassCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: ClassVar[int] = 1 self.assertEqual(1, Base._data0) def test_InitValue_DunderInit_ClassCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: ClassVar[int] = 1 def __init__(self) -> None: pass self.assertEqual(1, Base._data0) class WithSlots(TestCase): def test_InitValue_NoDunderInit_ClassCheck(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: ClassVar[int] = 1 self.assertEqual(1, Base._data0) def test_InitValue_DunderInit_ClassCheck(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: ClassVar[int] = 1 def __init__(self) -> None: pass self.assertEqual(1, Base._data0) def test_InitValue_InitOverwrite_InstantiationCheck(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: ClassVar[int] = 1 def __init__(self) -> None: self._data0 = 5 with self.assertRaises(AttributeError, msg="Class field '_data0' should not be accessible from within instance."): _ = Base() def test_InitValue_InitReadValue_InstantiationCheck(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: ClassVar[int] = 1 _data1: int def __init__(self) -> None: self._data1 = self._data0 + 5 self.assertEqual(1, Base._data0) inst = Base() self.assertEqual(6, inst._data1) class Inheritance_WithSlots(TestCase): def test_BaseAssigned(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: ClassVar[int] = 1 class Parent(Base): pass self.assertEqual(1, Base._data0) self.assertEqual(1, Parent._data0) base = Base() self.assertEqual(1, base._data0) parent = Parent() self.assertEqual(1, parent._data0) def test_BaseAssigned_ParentAssigned(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: ClassVar[int] = 1 class Parent(Base): _data0: ClassVar[int] = 2 self.assertEqual(1, Base._data0) self.assertEqual(2, Parent._data0) base = Base() self.assertEqual(1, base._data0) parent = Parent() self.assertEqual(2, parent._data0) pyTooling-8.11.0/tests/unit/MetaClasses/ObjectVariable.py000066400000000000000000000150141513317154500233460ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for class :class:`pyTooling.MetaClasses.ExtendedType`. """ from unittest import TestCase from pyTooling.MetaClasses import ExtendedType if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class WithoutSlots(TestCase): def test_NoInitValue_NoDunderInit_ClassCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: int with self.assertRaises(AttributeError, msg="Field '_data0' should not exist on class 'Base'."): _ = Base._data0 def test_NoInitValue_NoDunderInit_InstCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: int inst = Base() with self.assertRaises(AttributeError, msg="Field '_data0' shouldn't be initialized on instance."): _ = inst._data0 inst._data0 = 1 self.assertEqual(1, inst._data0) def test_NoInitValue_DunderInit_ClassCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: int def __init__(self) -> None: self._data0 = 1 with self.assertRaises(AttributeError, msg="Field '_data0' should not exist on class 'Base'."): _ = Base._data0 def test_NoInitValue_DunderInit_InstCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: int def __init__(self) -> None: self._data0 = 1 inst = Base() self.assertEqual(1, inst._data0) inst._data0 = 2 self.assertEqual(2, inst._data0) def test_InitValue_NoDunderInit_InstCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: int = 1 inst = Base() self.assertEqual(1, inst._data0) inst._data0 = 2 self.assertEqual(2, inst._data0) def test_InitValue_DunderInit_InstCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: int = 1 def __init__(self) -> None: pass inst = Base() self.assertEqual(1, inst._data0) inst._data0 = 2 self.assertEqual(2, inst._data0) def test_InitValue_InitOverwrite_InstCheck(self) -> None: class Base(metaclass=ExtendedType): _data0: int = 1 def __init__(self) -> None: self._data0 = 5 inst = Base() self.assertEqual(5, inst._data0) inst._data0 = 2 self.assertEqual(2, inst._data0) class WithSlots(TestCase): def test_NoInitValue_NoDunderInit_InstCheck(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: int inst = Base() with self.assertRaises(AttributeError, msg="Field '_data0' shouldn't be initialized on instance."): _ = inst._data0 inst._data0 = 1 self.assertEqual(1, inst._data0) def test_NoInitValue_DunderInit_InstCheck(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: int def __init__(self) -> None: self._data0 = 1 inst = Base() self.assertEqual(1, inst._data0) inst._data0 = 2 self.assertEqual(2, inst._data0) def test_InitValue_InitOverwrite_InstCheck(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data0: int = 1 def __init__(self) -> None: self._data0 = 5 inst = Base() self.assertEqual(5, inst._data0) inst._data0 = 2 self.assertEqual(2, inst._data0) pyTooling-8.11.0/tests/unit/MetaClasses/Overloading.py000066400000000000000000000101561513317154500227450ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for class :class:`pyTooling.MetaClasses.Overloading`. """ from unittest import TestCase from pyTooling.MetaClasses import ExtendedType if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Application(metaclass=ExtendedType): def __init__(self, x : int) -> None: self.x = x def __init__(self, x : str) -> None: self.x = x class Overloading(TestCase): def test_OverloadingByTypeSignature(self) -> None: app1 = Application(1) self.assertEqual(app1.x, 1) app2 = Application("2") self.assertEqual(app2.x, "2") pyTooling-8.11.0/tests/unit/MetaClasses/Serialization.py000066400000000000000000000211401513317154500233040ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for class :class:`pyTooling.MetaClasses.ExtendedType`. This test suite tests decorators: * :func:`@abstractmethod ` * :func:`@mustoverride ` """ from pickle import loads, dumps from typing import Dict, Any from unittest import TestCase from pyTooling.MetaClasses import ExtendedType, ExtendedTypeError from pyTooling.Graph import Graph, Vertex, Edge from pyTooling.Tree import Node if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class SimpleClass(metaclass=ExtendedType, slots=True): _data1: int def __init__(self, data: int) -> None: self._data1 = data class DerivedClass(SimpleClass): _data2: int def __init__(self, data: int) -> None: super().__init__(data) self._data2 = data + 1 class MixinClass(metaclass=ExtendedType, mixin=True): _data3: int def __init__(self, data: int) -> None: self._data3 = data + 2 class MixedClass(DerivedClass, MixinClass): _data4: int def __init__(self, data: int) -> None: super().__init__(data) MixinClass.__init__(self, data) self._data4 = data + 3 class LessFields(SimpleClass): def __setstate__(self, state: Dict[str, Any]): del state["_data1"] super().__setstate__(state) class MoreFields(SimpleClass): def __setstate__(self, state: Dict[str, Any]): state["more"] = -2 super().__setstate__(state) class Pickleing(TestCase): def test_SimpleClass(self) -> None: rootInstance = SimpleClass(10) serialized = dumps(rootInstance) recreatedInstance = loads(serialized) self.assertEqual(10, recreatedInstance._data1) def test_DerivedClass(self) -> None: rootInstance = DerivedClass(10) serialized = dumps(rootInstance) recreatedInstance = loads(serialized) self.assertEqual(10, recreatedInstance._data1) self.assertEqual(11, recreatedInstance._data2) def test_MixedClass(self) -> None: rootInstance = MixedClass(10) serialized = dumps(rootInstance) recreatedInstance = loads(serialized) self.assertEqual(10, recreatedInstance._data1) self.assertEqual(11, recreatedInstance._data2) self.assertEqual(12, recreatedInstance._data3) self.assertEqual(13, recreatedInstance._data4) def test_LessFields(self) -> None: rootInstance = LessFields(10) serialized = dumps(rootInstance) with self.assertRaises(ExtendedTypeError) as ex: _ = loads(serialized) self.assertIn("_data1", str(ex.exception)) def test_MoreFields(self) -> None: rootInstance = MoreFields(10) serialized = dumps(rootInstance) with self.assertRaises(ExtendedTypeError) as ex: _ = loads(serialized) self.assertIn("more", str(ex.exception)) def format(value: Node) -> str: return "" class PickledTree(TestCase): def test_SimpleTree(self) -> None: root = Node(value="Root") n1 = Node(nodeID=1, value="node1", keyValuePairs=(kvp1 := {"a": 1, "b": 2}), format=format, parent=root) n2 = Node(nodeID=2, value="node2", keyValuePairs=(kvp2 := {"g": 1, "h": 2}), format=format, parent=root) n3 = Node(nodeID=3, value="node3", keyValuePairs=(kvp3 := {"x": 1, "y": 2}), format=format, parent=root) serialized = dumps(root) recreated: Node = loads(serialized) self.assertEqual("Root", recreated.Value) self.assertEqual("node1", recreated.GetNodeByID(1).Value) self.assertEqual("node2", recreated.GetNodeByID(2).Value) self.assertEqual("node3", recreated.GetNodeByID(3).Value) self.assertIs(format, recreated.GetNodeByID(1)._format) self.assertDictEqual(kvp1, recreated.GetNodeByID(1)._dict) self.assertDictEqual(kvp2, recreated.GetNodeByID(2)._dict) self.assertDictEqual(kvp3, recreated.GetNodeByID(3)._dict) class PickledGraph(TestCase): def test_SimpleGraph(self) -> None: graph = Graph("Graph") v1 = Vertex(vertexID=1, value="v1", weight=120, keyValuePairs=(kvp1 := {"a": 1, "b": 2}), graph=graph) v2 = Vertex(vertexID=2, value="v2", weight=230, keyValuePairs=(kvp2 := {"g": 1, "h": 2}), graph=graph) v3 = Vertex(vertexID=3, value="v3", weight=310, keyValuePairs=(kvp3 := {"x": 1, "y": 2}), graph=graph) v1.EdgeToVertex(v2, edgeID=12, edgeValue="12", edgeWeight=125, keyValuePairs=(kvp12 := {"e": 1, "f": 2})) v2.EdgeToVertex(v3, edgeID=23, edgeValue="23", edgeWeight=235, keyValuePairs=(kvp23 := {"i": 1, "j": 2})) v3.EdgeToVertex(v1, edgeID=31, edgeValue="31", edgeWeight=315, keyValuePairs=(kvp31 := {"k": 1, "l": 2})) serialized = dumps(graph) recreated: Graph = loads(serialized) self.assertEqual("Graph", recreated.Name) self.assertEqual(3, graph.VertexCount) self.assertEqual(3, graph.EdgeCount) r1 = graph.GetVertexByID(1) r2 = graph.GetVertexByID(2) r3 = graph.GetVertexByID(3) self.assertIs((e12 := r1.OutboundEdges[0]).Destination, r2) self.assertIs((e23 := r2.OutboundEdges[0]).Destination, r3) self.assertIs((e31 := r3.OutboundEdges[0]).Destination, r1) self.assertDictEqual(kvp1, r1._dict) self.assertDictEqual(kvp2, r2._dict) self.assertDictEqual(kvp3, r3._dict) self.assertDictEqual(kvp12, e12._dict) self.assertDictEqual(kvp23, e23._dict) self.assertDictEqual(kvp31, e31._dict) pyTooling-8.11.0/tests/unit/MetaClasses/Singleton.py000066400000000000000000000153221513317154500224360ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for class :class:`pyTooling.MetaClasses.Singleton`. """ from unittest import TestCase from pytest import mark from pyTooling.MetaClasses import ExtendedType if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class App1WithoutParameters(metaclass=ExtendedType, singleton=True): X = 10 def __init__(self) -> None: print("Instance of 'App1WithoutParameters' was created") self.X = 11 class App2WithoutParameters(metaclass=ExtendedType, singleton=True): X = 20 def __init__(self) -> None: print("Instance of 'App2WithoutParameters' was created") self.X = 21 class App3WithParameters(metaclass=ExtendedType, singleton=True): X = 30 def __init__(self, x: int = 31) -> None: print("Instance of 'App1WithParameters' was created") self.X = x class DerivedApp2WithoutParameters(App2WithoutParameters): X = 120 def __init__(self) -> None: super().__init__() print("Instance of 'DerivedApp2WithoutParameters' was created") class DerivedApp3WithInnerParameters(App3WithParameters): X = 130 def __init__(self) -> None: super().__init__(x=131) print("Instance of 'DerivedApp3WithInnerParameters' was created") class DerivedApp3WithOuterParameters(App3WithParameters): X = 135 def __init__(self, x: int = 136) -> None: super().__init__(x) print("Instance of 'DerivedApp3WithOuterParameters' was created") class Singleton(TestCase): def test_CrossRelations(self) -> None: self.assertEqual(10, App1WithoutParameters.X) self.assertEqual(20, App2WithoutParameters.X) app_1 = App1WithoutParameters() self.assertEqual(11, app_1.X) app_1.X = 12 self.assertEqual(12, app_1.X) app_1same = App1WithoutParameters() self.assertIs(app_1, app_1same) self.assertEqual(12, app_1same.X) self.assertEqual(10, App1WithoutParameters.X) app_2 = App2WithoutParameters() self.assertIsNot(app_1, app_2) self.assertEqual(12, app_1.X) self.assertEqual(21, app_2.X) app_2.X = 22 self.assertEqual(12, app_1.X) self.assertEqual(22, app_2.X) def test_SecondInstanceWithParameters(self) -> None: # ensure at least one instance was created App1WithoutParameters() with self.assertRaises(ValueError) as ExceptionCapture: App1WithoutParameters(x = 35) self.assertEqual("A further instance of a singleton can't be reinitialized with parameters.", str(ExceptionCapture.exception)) def test_DerivedClassNoParameters(self) -> None: self.assertEqual(120, DerivedApp2WithoutParameters.X) app = DerivedApp2WithoutParameters() self.assertEqual(21, app.X) app.X = 22 self.assertEqual(22, app.X) app2 = DerivedApp2WithoutParameters() self.assertIs(app, app2) self.assertEqual(22, app2.X) self.assertEqual(120, DerivedApp2WithoutParameters.X) def test_DerivedClassWithInnerParameters(self) -> None: app = DerivedApp3WithInnerParameters() self.assertEqual(131, app.X) appSame = DerivedApp3WithInnerParameters() self.assertIs(app, appSame) @mark.xfail(reason="This case is not yet supported.") def test_DerivedClassWithOuterParameters(self) -> None: app = DerivedApp3WithOuterParameters(x=137) self.assertEqual(137, app.X) appSame = DerivedApp3WithOuterParameters() self.assertIs(app, appSame) pyTooling-8.11.0/tests/unit/MetaClasses/SlottedType.py000066400000000000000000000736631513317154500227700ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for class :class:`pyTooling.MetaClasses.ExtendedType`. """ from typing import Optional as Nullable from unittest import TestCase from pytest import mark from pyTooling.MetaClasses import ExtendedType, BaseClassIsNotAMixinError, BaseClassWithNonEmptySlotsError, BaseClassWithoutSlotsError from pyTooling.Common import getsizeof from pyTooling.Platform import CurrentPlatform if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class ObjectSizes(TestCase): class Normal1: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Normal2(Normal1): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 class Extended1(metaclass=ExtendedType): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Extended2(Extended1): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 class Slotted1(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Slotted2(Slotted1): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 SIZES = { Slotted1: { 3: {7: 84, 8: 68, 9: 68, 10: 68, 11: 68, 12: 68, 13: 68, 14: 68} }, Slotted2: { 3: {7: 92, 8: 76, 9: 76, 10: 76, 11: 76, 12: 76, 13: 76, 14: 76} } } @mark.skipif(CurrentPlatform.IsPyPy, reason="getsizeof: not supported on PyPy") def test_SizeOfSlotted1(self) -> None: data = self.Slotted1(data=5) pv = CurrentPlatform.PythonVersion dataSize = getsizeof(data) self.assertLessEqual( dataSize, self.SIZES[self.Slotted1][pv.Major][pv.Minor] ) print(f"\nsize: {dataSize} B") @mark.skipif(CurrentPlatform.IsPyPy, reason="getsizeof: not supported on PyPy") def test_SizeOfSlotted2(self) -> None: data = self.Slotted2(data=5) pv = CurrentPlatform.PythonVersion dataSize = getsizeof(data) self.assertLessEqual( dataSize, self.SIZES[self.Slotted2][pv.Major][pv.Minor] ) print(f"\nsize: {dataSize} B") @mark.skipif(CurrentPlatform.IsPyPy, reason="getsizeof: not supported on PyPy") def test_ClassSizes(self) -> None: print() print(f"size of Normal1: {getsizeof(self.Normal1)} B") print(f"size of Normal2: {getsizeof(self.Normal2)} B") print(f"size of Extended1: {getsizeof(self.Extended1)} B") print(f"size of Extended2: {getsizeof(self.Extended2)} B") print(f"size of Slotted1: {getsizeof(self.Slotted1)} B") print(f"size of Slotted2: {getsizeof(self.Slotted2)} B") class AttributeErrors(TestCase): class Data0(metaclass=ExtendedType, slots=True): _int_0: int class Data1(metaclass=ExtendedType, slots=True): _int_1: int def __init__(self) -> None: self._int_1 = 1 def method_11(self): self._str_1 = "foo" def method_12(self): _ = self._int_0 class Data2(Data1): #, slots=True): _int_2: int def __init__(self) -> None: super().__init__() self._int_2 = 2 def method_21(self): self._str_2 = "bar" def method_22(self): _ = self._int_0 def test_NormalField_1(self) -> None: data = self.Data1() self.assertEqual(1, data._int_1) def test_AddNewFieldInMethod_1(self) -> None: data = self.Data1() with self.assertRaises(AttributeError): data.method_11() def test_AddNewFieldByCode_1(self) -> None: data = self.Data1() with self.assertRaises(AttributeError): data._float1 = 3.4 def test_NormalField_2(self) -> None: data = self.Data2() self.assertEqual(1, data._int_1) self.assertEqual(2, data._int_2) def test_AddNewFieldInMethod_2(self) -> None: data = self.Data2() with self.assertRaises(AttributeError): data.method_21() def test_AddNewFieldByCode_2(self) -> None: data = self.Data2() with self.assertRaises(AttributeError): data._float2 = 4.3 def test_ReadNonExistingFieldInMethod_1(self) -> None: data = self.Data1() with self.assertRaises(AttributeError): data.method_12() def test_ReadNonExistingFieldInMethod_2(self) -> None: data = self.Data2() with self.assertRaises(AttributeError): data.method_22() def test_ReadNonExistingFieldByCode_1(self) -> None: data = self.Data1() with self.assertRaises(AttributeError): _ = data._int_0 def test_ReadNonExistingFieldByCode_2(self) -> None: data = self.Data2() with self.assertRaises(AttributeError): _ = data._int_0 def test_UninitializedSlot(self) -> None: data = self.Data0() with self.assertRaises(AttributeError): _ = data._int_0 data._int_0 = 1 _ = data._int_0 class Inheritance(TestCase): def test_LinearInheritance_1_BaseSlotted(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Final(Base): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 inst = Final(0) self.assertEqual(0, inst._data_0) self.assertEqual(1, inst._data_1) def test_LinearInheritance_2_BaseSlotted(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Parent(Base): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 class Final(Parent): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) self._data_2 = data + 2 inst = Final(1) self.assertEqual(1, inst._data_0) self.assertEqual(2, inst._data_1) self.assertEqual(3, inst._data_2) def test_LinearInheritance_1_BaseMixin(self) -> None: print() class Base(metaclass=ExtendedType, mixin=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Final(Base, mixin=True): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 # FIXME: why does it fail? # TODO: could be an instantiation error (TypeError) when collected slots (mixinSlots) are not set in __slots__ with self.assertRaises(AttributeError): inst = Final(0) self.assertEqual(0, inst._data_0) self.assertEqual(1, inst._data_1) def test_LinearInheritance_2_BaseMixin(self) -> None: class Base(metaclass=ExtendedType, mixin=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Parent(Base, mixin=True): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 class Final(Parent, mixin=True): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) self._data_2 = data + 2 # FIXME: why does it fail? # TODO: could be an instantiation error (TypeError) when collected slots (mixinSlots) are not set in __slots__ with self.assertRaises(AttributeError): inst = Final(1) self.assertEqual(1, inst._data_0) self.assertEqual(2, inst._data_1) self.assertEqual(3, inst._data_2) def test_LinearInheritance_1_BaseSlottedMixin(self) -> None: class Base(metaclass=ExtendedType, mixin=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Final(Base): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 inst = Final(0) self.assertEqual(0, inst._data_0) self.assertEqual(1, inst._data_1) def test_LinearInheritance_2_BaseSlottedMixin(self) -> None: class Base(metaclass=ExtendedType, mixin=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Parent(Base, mixin=True): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 class Final(Parent): _data_2: int def __init__(self, data: int) -> None: bs = Base.__slots__ bm = Base.__mixinSlots__ ps = Parent.__slots__ pm = Parent.__mixinSlots__ fs = Final.__slots__ super().__init__(data) self._data_2 = data + 2 inst = Final(1) self.assertEqual(1, inst._data_0) self.assertEqual(2, inst._data_1) self.assertEqual(3, inst._data_2) def test_LinearInheritance_1_BaseMixin_FinalSlotted(self) -> None: class Base(metaclass=ExtendedType, mixin=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Final(Base): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 inst = Final(0) self.assertEqual(0, inst._data_0) self.assertEqual(1, inst._data_1) def test_LinearInheritance_2_BaseMixin_FinalSlotted(self) -> None: class Base(metaclass=ExtendedType, mixin=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Parent(Base, mixin=True): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 class Final(Parent): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) self._data_2 = data + 2 inst = Final(1) self.assertEqual(1, inst._data_0) self.assertEqual(2, inst._data_1) self.assertEqual(3, inst._data_2) def test_VInheritance_PrimaryExtended(self) -> None: class Primary(metaclass=ExtendedType, slots=True): _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary: _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 with self.assertRaises(BaseClassWithoutSlotsError): class Final(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 # inst = Final(2) # self.assertEqual(2, inst._data_L0) # self.assertEqual(3, inst._data_R0) # self.assertEqual(4, inst._data_1) def test_VInheritance_PrimaryExtended_Mixin(self) -> None: class Primary(metaclass=ExtendedType, slots=True): _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary(metaclass=ExtendedType, mixin=True): _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 class Final(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 inst = Final(2) self.assertEqual(2, inst._data_L0) self.assertEqual(3, inst._data_R0) self.assertEqual(4, inst._data_1) def test_VInheritance_SecondaryExtended(self) -> None: class Primary: _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary(metaclass=ExtendedType, slots=True): _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 class Final(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 inst = Final(3) self.assertEqual(3, inst._data_L0) self.assertEqual(4, inst._data_R0) self.assertEqual(5, inst._data_1) def test_YInheritance_PrimaryExtended(self) -> None: class Primary(metaclass=ExtendedType, slots=True): _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary: _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 with self.assertRaises(BaseClassWithoutSlotsError): class Merged(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 # class Final(Merged): # _data_2: int # # def __init__(self, data: int) -> None: # super().__init__(data) # self._data_2 = data + 3 # # inst = Final(4) # self.assertEqual(4, inst._data_L0) # self.assertEqual(5, inst._data_R0) # self.assertEqual(6, inst._data_1) # self.assertEqual(7, inst._data_2) def test_YInheritance_PrimaryExtended_Mixin(self) -> None: class Primary(metaclass=ExtendedType, slots=True): _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary(metaclass=ExtendedType, mixin=True): _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 class Merged(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 class Final(Merged): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) self._data_2 = data + 3 inst = Final(4) self.assertEqual(4, inst._data_L0) self.assertEqual(5, inst._data_R0) self.assertEqual(6, inst._data_1) self.assertEqual(7, inst._data_2) def test_YInheritance_SecondaryExtended(self) -> None: class Primary: _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary(metaclass=ExtendedType, slots=True): _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 class Merged(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 class Final(Merged): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) self._data_2 = data + 3 inst = Final(5) self.assertEqual(5, inst._data_L0) self.assertEqual(6, inst._data_R0) self.assertEqual(7, inst._data_1) self.assertEqual(8, inst._data_2) def test_OInheritance_BaseExtended(self) -> None: print() class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 with self.assertRaises(BaseClassWithNonEmptySlotsError): #BaseClassIsNotAMixinError): class Final(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 # inst = Final(6) # for m in Final.mro(): # print(m) # self.assertEqual(6, inst._data_0) # self.assertEqual(7, inst._data_L1) # self.assertEqual(8, inst._data_R1) # self.assertEqual(9, inst._data_2) def test_OInheritance_BaseExtended_PrimaryMixin(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base, mixin=True): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 with self.assertRaises(BaseClassWithNonEmptySlotsError): class Final(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 # inst = Final(6) # for m in Final.mro(): # print(m) # self.assertEqual(6, inst._data_0) # self.assertEqual(7, inst._data_L1) # self.assertEqual(8, inst._data_R1) # self.assertEqual(9, inst._data_2) def test_OInheritance_BaseExtended_SecondaryMixin(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base, mixin=True): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Final(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 inst = Final(6) for m in Final.mro(): print(m) self.assertEqual(6, inst._data_0) self.assertEqual(7, inst._data_L1) self.assertEqual(8, inst._data_R1) self.assertEqual(9, inst._data_2) def test_OInheritance_PrimaryExtended(self) -> None: class Base: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data with self.assertRaises(BaseClassWithoutSlotsError): class Primary(Base, metaclass=ExtendedType, slots=True): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 # class Secondary(Base): # _data_R1: int # # def __init__(self, data: int) -> None: # super().__init__(data) # self._data_R1 = data + 2 # # class Final(Primary, Secondary): # _data_2: int # # def __init__(self, data: int) -> None: # super().__init__(data) # Secondary.__init__(self, data) # self._data_2 = data + 3 # # inst = Final(7) # self.assertEqual(7, inst._data_0) # self.assertEqual(8, inst._data_L1) # self.assertEqual(9, inst._data_R1) # self.assertEqual(10, inst._data_2) def test_OInheritance_PrimaryExtended_Slots_Mixin(self) -> None: class Base: _data_0: int __slots__ = ("_data_0", ) def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base, metaclass=ExtendedType, slots=True): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base, metaclass=ExtendedType, mixin=True): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Final(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 inst = Final(7) self.assertEqual(7, inst._data_0) self.assertEqual(8, inst._data_L1) self.assertEqual(9, inst._data_R1) self.assertEqual(10, inst._data_2) def test_OInheritance_SecondaryExtended(self) -> None: class Base: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 with self.assertRaises(BaseClassWithoutSlotsError): class Secondary(Base, metaclass=ExtendedType, slots=True): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 # class Final(Primary, Secondary): # _data_2: int # # def __init__(self, data: int) -> None: # super().__init__(data) # Secondary.__init__(self, data) # self._data_2 = data + 3 # # inst = Final(8) # self.assertEqual(8, inst._data_0) # self.assertEqual(9, inst._data_L1) # self.assertEqual(10, inst._data_R1) # self.assertEqual(11, inst._data_2) def test_OInheritance_SecondaryExtended_Slots_Slots(self) -> None: class Base: _data_0: int __slots__ = ("_data_0", ) def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int # __slots__ = () # __mixinSlots__ = ("_data_L1") def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base, metaclass=ExtendedType, slots=True): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Final(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 inst = Final(8) self.assertEqual(8, inst._data_0) self.assertEqual(9, inst._data_L1) self.assertEqual(10, inst._data_R1) self.assertEqual(11, inst._data_2) def test_OInheritance_MergedExtended(self) -> None: class Base: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 with self.assertRaises(BaseClassWithoutSlotsError): class Final(Primary, Secondary, metaclass=ExtendedType, slots=True): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 # inst = Final(9) # self.assertEqual(9, inst._data_0) # self.assertEqual(10, inst._data_L1) # self.assertEqual(11, inst._data_R1) # self.assertEqual(12, inst._data_2) def test_QInheritance_BaseExtended(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 with self.assertRaises(BaseClassWithNonEmptySlotsError): #BaseClassIsNotAMixinError): class Merged(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 # class Final(Merged): # _data_3: int # # def __init__(self, data: int) -> None: # super().__init__(data) # self._data_3 = data + 4 # # inst = Final(10) # self.assertEqual(10, inst._data_0) # self.assertEqual(11, inst._data_L1) # self.assertEqual(12, inst._data_R1) # self.assertEqual(13, inst._data_2) # self.assertEqual(14, inst._data_3) def test_QInheritance_BaseExtended_PrimaryMixin(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base, mixin=True): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 with self.assertRaises(BaseClassWithNonEmptySlotsError): class Merged(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 # class Final(Merged): # _data_3: int # # def __init__(self, data: int) -> None: # super().__init__(data) # self._data_3 = data + 4 # # inst = Final(10) # self.assertEqual(10, inst._data_0) # self.assertEqual(11, inst._data_L1) # self.assertEqual(12, inst._data_R1) # self.assertEqual(13, inst._data_2) # self.assertEqual(14, inst._data_3) def test_QInheritance_BaseExtended_SecondaryMixin(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base, mixin=True): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Merged(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 class Final(Merged): _data_3: int def __init__(self, data: int) -> None: super().__init__(data) self._data_3 = data + 4 inst = Final(10) self.assertEqual(10, inst._data_0) self.assertEqual(11, inst._data_L1) self.assertEqual(12, inst._data_R1) self.assertEqual(13, inst._data_2) self.assertEqual(14, inst._data_3) def test_QInheritance_FinalExtended(self) -> None: class Base: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Merged(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 with self.assertRaises(BaseClassWithoutSlotsError): class Final(Merged, metaclass=ExtendedType, slots=True): _data_3: int def __init__(self, data: int) -> None: super().__init__(data) self._data_3 = data + 4 # inst = Final(14) # self.assertEqual(14, inst._data_0) # self.assertEqual(15, inst._data_L1) # self.assertEqual(16, inst._data_R1) # self.assertEqual(17, inst._data_2) # self.assertEqual(18, inst._data_3) class Hierarchy(TestCase): def test_GraphMLInheritanceHierarchy(self) -> None: class Base(metaclass=ExtendedType, slots=True): _data_0: int def __init__(self) -> None: super().__init__() self._data_0 = 0 class WithID(Base): _data_1: int def __init__(self) -> None: super().__init__() self._data_1 = 1 class WithData(WithID): _data_2: int def __init__(self) -> None: super().__init__() self._data_2 = 2 class Node(WithData): _data_3: int def __init__(self) -> None: super().__init__() self._data_3 = 3 class BaseGraph(WithData, mixin=True): _data_4: int def __init__(self, param: Nullable[str] = None) -> None: if param is not None: super().__init__() self._data_4 = 4 def test_BaseGraph(self) -> None: self._data_4 = 14 class SubGraph(Node, BaseGraph): _data_5: int def __init__(self) -> None: super().__init__() BaseGraph.__init__(self) self._data_5 = 5 sg = SubGraph() sg.test_BaseGraph() def test_YAMLConfigurationInheritanceHierarchy(self) -> None: class Node0(metaclass=ExtendedType, slots=True): _data_0: int class Dict0(Node0, mixin=True): _data_10: int class Config0(Node0, mixin=True): _data_11: int class Node(Node0): _data_2: int class Dict(Node, Dict0): _data_3: int class Config(Dict, Config0): _data_4: int c = Config() pyTooling-8.11.0/tests/unit/MetaClasses/__init__.py000066400000000000000000000430421513317154500222330ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ ____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | \/ | ___| |_ __ _ / ___| | __ _ ___ ___ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | | | |/ _` / __/ __|/ _ \/ __| # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | | | __/ || (_| | |___| | (_| \__ \__ \ __/\__ \ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/ # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for class :class:`pyTooling.MetaClasses.ExtendedType`. """ from sys import version_info from typing import Any from unittest import TestCase from pyTooling.MetaClasses import ExtendedType if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class WithoutSlots(TestCase): # WORKAROUND: for Python versions before 3.14 if version_info < (3, 14): def assertHasAttr(self, obj: object, attr: str, msg: Any = None) -> None: self.assertTrue(hasattr(obj, attr)) def assertNotHasAttr(self, obj: object, attr: str, msg: Any = None) -> None: self.assertFalse(hasattr(obj, attr)) def test_NoInheritance(self) -> None: class Base(metaclass=ExtendedType): pass self.assertHasAttr(Base, "__slotted__") self.assertFalse(Base.__slotted__) self.assertNotHasAttr(Base, "__allSlots__") self.assertNotHasAttr(Base, "__slots__") self.assertHasAttr(Base, "__isMixin__") self.assertFalse(Base.__isMixin__) self.assertHasAttr(Base, "__mixinSlots__") self.assertIsInstance(Base.__mixinSlots__, tuple) self.assertHasAttr(Base, "__methods__") self.assertIsInstance(Base.__methods__, tuple) self.assertHasAttr(Base, "__methodsWithAttributes__") self.assertIsInstance(Base.__methodsWithAttributes__, tuple) self.assertHasAttr(Base, "__abstractMethods__") self.assertIsInstance(Base.__abstractMethods__, dict) self.assertHasAttr(Base, "__isAbstract__") self.assertFalse(Base.__isAbstract__) self.assertHasAttr(Base, "__isSingleton__") self.assertFalse(Base.__isSingleton__) self.assertNotHasAttr(Base, "__singletonInstanceCond__") self.assertNotHasAttr(Base, "__singletonInstanceInit__") self.assertNotHasAttr(Base, "__singletonInstanceCache__") self.assertHasAttr(Base, "__pyattr__") inst = Base() self.assertIsNotNone(inst) def test_NoInheritance_Init1(self) -> None: class Base(metaclass=ExtendedType): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data inst = Base(0) self.assertEqual(0, inst._data_0) def test_LinearInheritance_1(self) -> None: class Base(metaclass=ExtendedType): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Final(Base): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 inst = Final(0) self.assertEqual(0, inst._data_0) self.assertEqual(1, inst._data_1) def test_LinearInheritance_2(self) -> None: class Base(metaclass=ExtendedType): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Parent(Base): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_1 = data + 1 class Final(Parent): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) self._data_2 = data + 2 inst = Final(1) self.assertEqual(1, inst._data_0) self.assertEqual(2, inst._data_1) self.assertEqual(3, inst._data_2) def test_VInheritance_PrimaryExtended(self) -> None: class Primary(metaclass=ExtendedType): _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary: _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 class Final(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 inst = Final(2) self.assertEqual(2, inst._data_L0) self.assertEqual(3, inst._data_R0) self.assertEqual(4, inst._data_1) def test_VInheritance_SecondaryExtended(self) -> None: class Primary: _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary(metaclass=ExtendedType): _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 class Final(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 inst = Final(3) self.assertEqual(3, inst._data_L0) self.assertEqual(4, inst._data_R0) self.assertEqual(5, inst._data_1) def test_YInheritance_PrimaryExtended(self) -> None: class Primary(metaclass=ExtendedType): _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary: _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 class Merged(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 class Final(Merged): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) self._data_2 = data + 3 inst = Final(4) self.assertEqual(4, inst._data_L0) self.assertEqual(5, inst._data_R0) self.assertEqual(6, inst._data_1) self.assertEqual(7, inst._data_2) def test_YInheritance_SecondaryExtended(self) -> None: class Primary: _data_L0: int def __init__(self, data: int) -> None: self._data_L0 = data class Secondary(metaclass=ExtendedType): _data_R0: int def __init__(self, data: int) -> None: self._data_R0 = data + 1 class Merged(Primary, Secondary): _data_1: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_1 = data + 2 class Final(Merged): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) self._data_2 = data + 3 inst = Final(5) self.assertEqual(5, inst._data_L0) self.assertEqual(6, inst._data_R0) self.assertEqual(7, inst._data_1) self.assertEqual(8, inst._data_2) def test_OInheritance_BaseExtended(self) -> None: class Base(metaclass=ExtendedType): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Final(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 inst = Final(6) for m in Final.mro(): print(m) self.assertEqual(6, inst._data_0) self.assertEqual(7, inst._data_L1) self.assertEqual(8, inst._data_R1) self.assertEqual(9, inst._data_2) def test_OInheritance_PrimaryExtended(self) -> None: class Base: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base, metaclass=ExtendedType): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Final(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 inst = Final(7) self.assertEqual(7, inst._data_0) self.assertEqual(8, inst._data_L1) self.assertEqual(9, inst._data_R1) self.assertEqual(10, inst._data_2) def test_OInheritance_SecondaryExtended(self) -> None: class Base: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base, metaclass=ExtendedType): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Final(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 inst = Final(8) self.assertEqual(8, inst._data_0) self.assertEqual(9, inst._data_L1) self.assertEqual(10, inst._data_R1) self.assertEqual(11, inst._data_2) def test_OInheritance_MergedExtended(self) -> None: class Base: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Final(Primary, Secondary, metaclass=ExtendedType): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 inst = Final(9) self.assertEqual(9, inst._data_0) self.assertEqual(10, inst._data_L1) self.assertEqual(11, inst._data_R1) self.assertEqual(12, inst._data_2) def test_QInheritance_BaseExtended(self) -> None: class Base(metaclass=ExtendedType): _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Merged(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 class Final(Merged): _data_3: int def __init__(self, data: int) -> None: super().__init__(data) self._data_3 = data + 4 inst = Final(10) self.assertEqual(10, inst._data_0) self.assertEqual(11, inst._data_L1) self.assertEqual(12, inst._data_R1) self.assertEqual(13, inst._data_2) self.assertEqual(14, inst._data_3) def test_QInheritance_FinalExtended(self) -> None: class Base: _data_0: int def __init__(self, data: int) -> None: self._data_0 = data class Primary(Base): _data_L1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_L1 = data + 1 class Secondary(Base): _data_R1: int def __init__(self, data: int) -> None: super().__init__(data) self._data_R1 = data + 2 class Merged(Primary, Secondary): _data_2: int def __init__(self, data: int) -> None: super().__init__(data) Secondary.__init__(self, data) self._data_2 = data + 3 class Final(Merged, metaclass=ExtendedType): _data_3: int def __init__(self, data: int) -> None: super().__init__(data) self._data_3 = data + 4 inst = Final(14) self.assertEqual(14, inst._data_0) self.assertEqual(15, inst._data_L1) self.assertEqual(16, inst._data_R1) self.assertEqual(17, inst._data_2) self.assertEqual(18, inst._data_3) class WithSlots(TestCase): # WORKAROUND: for Python versions before 3.14 if version_info < (3, 14): def assertHasAttr(self, obj: object, attr: str, msg: Any = None) -> None: self.assertTrue(hasattr(obj, attr)) def assertNotHasAttr(self, obj: object, attr: str, msg: Any = None) -> None: self.assertFalse(hasattr(obj, attr)) def test_NoInheritance(self) -> None: class Base(metaclass=ExtendedType, slots=True): pass self.assertHasAttr(Base, "__slotted__") self.assertTrue(Base.__slotted__) self.assertHasAttr(Base, "__allSlots__") self.assertIsInstance(Base.__allSlots__, set) self.assertHasAttr(Base, "__slots__") self.assertIsInstance(Base.__slots__, tuple) self.assertHasAttr(Base, "__isMixin__") self.assertFalse(Base.__isMixin__) self.assertHasAttr(Base, "__mixinSlots__") self.assertIsInstance(Base.__mixinSlots__, tuple) self.assertHasAttr(Base, "__methods__") self.assertIsInstance(Base.__methods__, tuple) self.assertHasAttr(Base, "__methodsWithAttributes__") self.assertIsInstance(Base.__methodsWithAttributes__, tuple) self.assertHasAttr(Base, "__abstractMethods__") self.assertIsInstance(Base.__abstractMethods__, dict) self.assertHasAttr(Base, "__isAbstract__") self.assertFalse(Base.__isAbstract__) self.assertHasAttr(Base, "__isSingleton__") self.assertFalse(Base.__isSingleton__) self.assertNotHasAttr(Base, "__singletonInstanceCond__") self.assertNotHasAttr(Base, "__singletonInstanceInit__") self.assertNotHasAttr(Base, "__singletonInstanceCache__") self.assertHasAttr(Base, "__pyattr__") inst = Base() self.assertIsNotNone(inst) class Mixin(TestCase): # WORKAROUND: for Python versions before 3.14 if version_info < (3, 14): def assertHasAttr(self, obj: object, attr: str, msg: Any = None) -> None: self.assertTrue(hasattr(obj, attr)) def assertNotHasAttr(self, obj: object, attr: str, msg: Any = None) -> None: self.assertFalse(hasattr(obj, attr)) def test_NoInheritance(self) -> None: class Base(metaclass=ExtendedType, mixin=True): pass self.assertHasAttr(Base, "__slotted__") self.assertTrue(Base.__slotted__) self.assertHasAttr(Base, "__allSlots__") self.assertIsInstance(Base.__allSlots__, set) self.assertHasAttr(Base, "__slots__") self.assertIsInstance(Base.__slots__, tuple) self.assertHasAttr(Base, "__isMixin__") self.assertTrue(Base.__isMixin__) self.assertHasAttr(Base, "__mixinSlots__") self.assertIsInstance(Base.__mixinSlots__, tuple) self.assertHasAttr(Base, "__methods__") self.assertIsInstance(Base.__methods__, tuple) self.assertHasAttr(Base, "__methodsWithAttributes__") self.assertIsInstance(Base.__methodsWithAttributes__, tuple) self.assertHasAttr(Base, "__abstractMethods__") self.assertIsInstance(Base.__abstractMethods__, dict) self.assertHasAttr(Base, "__isAbstract__") self.assertFalse(Base.__isAbstract__) self.assertHasAttr(Base, "__isSingleton__") self.assertFalse(Base.__isSingleton__) self.assertNotHasAttr(Base, "__singletonInstanceCond__") self.assertNotHasAttr(Base, "__singletonInstanceInit__") self.assertNotHasAttr(Base, "__singletonInstanceCache__") self.assertHasAttr(Base, "__pyattr__") inst = Base() self.assertIsNotNone(inst) pyTooling-8.11.0/tests/unit/Packaging/000077500000000000000000000000001513317154500175775ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Packaging/__init__.py000066400000000000000000000230471513317154500217160ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ __ _ ___| | ____ _ __ _(_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_) / _` |/ __| |/ / _` |/ _` | | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| __/ (_| | (__| < (_| | (_| | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| \__,_|\___|_|\_\__,_|\__, |_|_| |_|\__, | # # |_| |___/ |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """ Unit tests for the packaging helper functions. """ from pathlib import Path from unittest import TestCase from pytest import mark from pyTooling.Platform import CurrentPlatform if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class HelperFunctions(TestCase): @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_VersionInformation(self) -> None: from pyTooling.Packaging import extractVersionInformation versionInformation = extractVersionInformation(Path("pyTooling/Common/__init__.py")) self.assertIsInstance(versionInformation.Keywords, list) self.assertEqual(43, len(versionInformation.Keywords)) @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_loadReadmeTXT(self) -> None: from pyTooling.Packaging import loadReadmeFile readme = loadReadmeFile(Path("tests/pyPackage/README.txt")) self.assertIn("1. pyPackage", readme.Content) self.assertEqual("text/plain", readme.MimeType) @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_loadReadmeMD(self) -> None: from pyTooling.Packaging import loadReadmeFile readme = loadReadmeFile(Path("tests/pyPackage/README.md")) self.assertIn("# pyPackage", readme.Content) self.assertEqual("text/markdown", readme.MimeType) @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_loadReadmeReST(self) -> None: from pyTooling.Packaging import loadReadmeFile readme = loadReadmeFile(Path("tests/pyPackage/README.rst")) self.assertIn("pyPackage", readme.Content) self.assertIn("#########", readme.Content) self.assertEqual("text/x-rst", readme.MimeType) @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_loadReadmeOther(self) -> None: from pyTooling.Packaging import loadReadmeFile with self.assertRaises(ValueError): _ = loadReadmeFile(Path("tests/pyPackage/README.ascii")) @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_loadRequirements(self) -> None: from pyTooling.Packaging import loadRequirementsFile requirements = loadRequirementsFile(Path("doc/requirements.txt")) self.assertEqual(13, len(requirements)) @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_loadRequirementsGit(self) -> None: from pyTooling.Packaging import loadRequirementsFile requirements = loadRequirementsFile(Path("tests/data/Requirements/requirements.Git.txt")) self.assertEqual(2, len(requirements)) @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_loadRequirementsRemoteZIP(self) -> None: from pyTooling.Packaging import loadRequirementsFile requirements = loadRequirementsFile(Path("tests/data/Requirements/requirements.HTTPS-ZIP.txt")) self.assertEqual(1, len(requirements)) @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_loadRequirementsRecursive(self) -> None: from pyTooling.Packaging import loadRequirementsFile requirements = loadRequirementsFile(Path("tests/data/Requirements/requirements.txt"), debug=True) self.assertEqual(5, len(requirements)) class VersionInformation(TestCase): @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_VersionInformation(self) -> None: from pyTooling.Packaging import VersionInformation versionInfo = VersionInformation( author="Author", email="email", copyright="copyright", license="license", version="0.0.1", description="description", keywords=["keyword1", "keyword2"] ) self.assertEqual("Author", versionInfo.Author) self.assertEqual("email", versionInfo.Email) self.assertEqual("copyright", versionInfo.Copyright) self.assertEqual("license", versionInfo.License) self.assertEqual("0.0.1", versionInfo.Version) self.assertEqual("description", versionInfo.Description) self.assertListEqual(["keyword1", "keyword2"], versionInfo.Keywords) class DescribePackage(TestCase): @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_PythonPackage(self) -> None: print() from pyTooling.Packaging import DescribePythonPackage packageName = "pyPackage.Tool" packagePath = Path("tests") / Path(packageName) packageInformation = DescribePythonPackage( packageName=packageName, description="Swiss army knife.", projectURL="https://", sourceCodeURL="https://", documentationURL="https://", issueTrackerCodeURL="https://", sourceFileWithVersion=packagePath / "__init__.py", keywords=("Swiss", "Knife") ) self.assertEqual(16, len(packageInformation)) self.assertEqual(packageName, packageInformation["name"]) # TODO: more checks @mark.xfail(CurrentPlatform.IsMSYS2Environment, reason="Can fail on MSYS2 environment with Python 3.10+.") def test_PythonPackageFromGitHub(self) -> None: print() from pyTooling.Packaging import DescribePythonPackageHostedOnGitHub packageName = "pyPackage" packagePath = Path("tests") / Path(packageName) packageInformation = DescribePythonPackageHostedOnGitHub( packageName=packageName, description="Swiss army knife.", gitHubNamespace=packageName, gitHubRepository=packageName, sourceFileWithVersion=packagePath / "__init__.py", requirementsFile=packagePath / "requirements.txt", documentationRequirementsFile=packagePath / "requirements.Doc.txt", unittestRequirementsFile=packagePath / "requirements.Test.txt", packagingRequirementsFile=packagePath / "requirements.Build.txt", additionalRequirements={ "dist": ["Wheel"], } ) self.assertEqual(16, len(packageInformation)) self.assertEqual(packageName, packageInformation["name"]) # TODO: more checks pyTooling-8.11.0/tests/unit/Path/000077500000000000000000000000001513317154500166075ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Path/URL.py000066400000000000000000000272161513317154500176330ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ ____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| ___ _ __ ___ _ __(_) ___| _ \ __ _| |_| |__ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _ / _ \ '_ \ / _ \ '__| |/ __| |_) / _` | __| '_ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | __/ | | | __/ | | | (__| __/ (_| | |_| | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|\___|_| |_|\___|_| |_|\___|_| \__,_|\__|_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for :mod:`pyTooling.GenericPath.URL`.""" from unittest import TestCase from pyTooling.GenericPath.URL import URL, Protocols if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class GenericPath(TestCase): url : URL = URL.Parse("https://pyTooling.GitHub.io:8080/path/to/endpoint?user=paebbels&token=1234567890") def test_Protocol(self) -> None: self.assertEqual(self.url.Scheme, Protocols.HTTPS) def test_Port(self) -> None: self.assertEqual(self.url.Host.Port, 8080) def test_Hostname(self) -> None: self.assertEqual(self.url.Host.Hostname, "pyTooling.GitHub.io") def test_str(self) -> None: self.assertEqual(str(self.url), "https://pyTooling.GitHub.io:8080/path/to/endpoint?user=paebbels&token=1234567890") class URLs(TestCase): def test_Host(self) -> None: resource = "github" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("github", url.Host.Hostname) self.assertIsNone(url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_IP(self) -> None: resource = "192.168.1.1" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("192.168.1.1", url.Host.Hostname) self.assertIsNone(url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_DNS(self) -> None: resource = "github.com" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertIsNone(url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_DNS_Port(self) -> None: resource = "github.com:80" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertEqual(80, url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_DNS_Port_Path(self) -> None: resource = "github.com:80/entrypoint" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertEqual(80, url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("/entrypoint", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_DNS_Port_Path_File(self) -> None: resource = "github.com:80/path/file.png" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertEqual(80, url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("/path/file.png", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_DNS_Port_Path_File_Query(self) -> None: resource = "github.com:80/path/file.png?width=1024" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertEqual(80, url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("/path/file.png", str(url.Path)) self.assertDictEqual({"width": "1024"}, url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_DNS_Port_Path_File_QueryQuery(self) -> None: resource = "github.com:80/path/file.png?width=1024&height=912" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertEqual(80, url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("/path/file.png", str(url.Path)) self.assertDictEqual({"width": "1024", "height": "912"}, url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_DNS_Port_Path_File_Fragment(self) -> None: resource = "github.com:80/entrypoint#chapter-3" url = URL.Parse(resource) self.assertIsNone(url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertEqual(80, url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("/entrypoint", str(url.Path)) self.assertIsNone(url.Query) self.assertEqual("chapter-3", url.Fragment) self.assertEqual(resource, str(url)) def test_HTTP_DNS(self) -> None: resource = "http://github.com" url = URL.Parse(resource) self.assertEqual(Protocols.HTTP, url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertIsNone(url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_HTTPS_DNS(self) -> None: resource = "https://github.com" url = URL.Parse(resource) self.assertEqual(Protocols.HTTPS, url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertIsNone(url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_HTTPS_DNS_Port(self) -> None: resource = "https://github.com:443" url = URL.Parse(resource) self.assertEqual(Protocols.HTTPS, url.Scheme) self.assertEqual("github.com", url.Host.Hostname) self.assertEqual(443, url.Host.Port) self.assertIsNone(url.User) self.assertIsNone(url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_HTTPS_User_DNS_Port(self) -> None: resource = "https://paebbels@v-4.github.com:25005" url = URL.Parse(resource) self.assertEqual(Protocols.HTTPS, url.Scheme) self.assertEqual("v-4.github.com", url.Host.Hostname) self.assertEqual(25005, url.Host.Port) self.assertEqual("paebbels", url.User) self.assertIsNone(url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_HTTPS_User_Pwd_DNS(self) -> None: resource = "https://paebbels:foobar@v4.api.github.com" url = URL.Parse(resource) self.assertEqual(Protocols.HTTPS, url.Scheme) self.assertEqual("v4.api.github.com", url.Host.Hostname) self.assertIsNone(url.Host.Port) self.assertEqual("paebbels", url.User) self.assertEqual("foobar", url.Password) self.assertEqual("", str(url.Path)) self.assertIsNone(url.Query) self.assertIsNone(url.Fragment) self.assertEqual(resource, str(url)) def test_GitLabCIToken(self) -> None: resource = "https://gitlab-ci-token:glcbt-64_2yjksyWRz6mPq57YFsvx@gitlab.company.com/path/to/resource.ext?query1=34&query2=343#ref-45" url = URL.Parse(resource) self.assertEqual(Protocols.HTTPS, url.Scheme) self.assertEqual("gitlab.company.com", url.Host.Hostname) self.assertIsNone(url.Host.Port) self.assertEqual("gitlab-ci-token", url.User) self.assertEqual("glcbt-64_2yjksyWRz6mPq57YFsvx", url.Password) self.assertEqual("/path/to/resource.ext", str(url.Path)) self.assertDictEqual({"query1": "34", "query2": "343"}, url.Query) self.assertEqual("ref-45", url.Fragment) self.assertEqual(resource, str(url)) cleanURL = url.WithoutCredentials() self.assertEqual(Protocols.HTTPS, url.Scheme) self.assertEqual("gitlab.company.com", url.Host.Hostname) self.assertIsNone(url.Host.Port) self.assertIsNone(cleanURL.User) self.assertIsNone(cleanURL.Password) self.assertEqual("/path/to/resource.ext", str(url.Path)) self.assertDictEqual({"query1": "34", "query2": "343"}, url.Query) self.assertEqual("ref-45", url.Fragment) pyTooling-8.11.0/tests/unit/Platform/000077500000000000000000000000001513317154500174775ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Platform/__init__.py000066400000000000000000000615041513317154500216160ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ ____ _ _ __ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \| | __ _| |_ / _| ___ _ __ _ __ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_) | |/ _` | __| |_ / _ \| '__| '_ ` _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| __/| | (_| | |_| _| (_) | | | | | | | | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|\__,_|\__|_| \___/|_| |_| |_| |_| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for TBD.""" from os import getenv as os_getenv, environ as os_environ from pytest import mark from unittest import TestCase from pyTooling.Platform import Platforms, Platform, CurrentPlatform if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class AnyPlatform(TestCase): expected = os_getenv("ENVIRONMENT_NAME", default="Windows (x86-64)") isOnGitHub = "GITHUB_SHA" in os_environ @mark.skipif(os_getenv("ENVIRONMENT_NAME", "skip") == "skip", reason="Skipped, if environment variable 'ENVIRONMENT_NAME' isn't set.") def test_PlatformString(self) -> None: platform = CurrentPlatform self.assertEqual(self.expected, str(platform)) @mark.skipif("Linux (x86-64)" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_NativeLinux_x86_64', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_NativeLinux_x86_64(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Linux_x86_64 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Linux_x86_64 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertTrue(platform.IsNativePlatform) self.assertFalse(platform.IsNativeWindows) self.assertTrue(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertFalse(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("", platform.ExecutableExtension) self.assertEqual("a", platform.StaticLibraryExtension) self.assertEqual("so", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Linux (aarch64)" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_NativeLinux_aarch64', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_NativeLinux_aarch64(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Linux_AArch64 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Linux_AArch64 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertTrue(platform.IsNativePlatform) self.assertFalse(platform.IsNativeWindows) self.assertTrue(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertFalse(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("", platform.ExecutableExtension) self.assertEqual("a", platform.StaticLibraryExtension) self.assertEqual("so", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("macOS (x86-64)" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_NativeMacOS_Intel', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_NativeMacOS_Intel(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.MacOS_Intel | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.MacOS_Intel | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertTrue(platform.IsNativePlatform) self.assertFalse(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertTrue(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertFalse(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("", platform.ExecutableExtension) self.assertEqual("a", platform.StaticLibraryExtension) self.assertEqual("dylib", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("macOS (aarch64)" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_NativeMacOS_ARM', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_NativeMacOS_ARM(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.MacOS_ARM | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.MacOS_ARM | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertTrue(platform.IsNativePlatform) self.assertFalse(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertTrue(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertFalse(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("", platform.ExecutableExtension) self.assertEqual("a", platform.StaticLibraryExtension) self.assertEqual("dylib", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Windows (x86-64)" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_NativeWindows_x86_64', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_NativeWindows_x86_64(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Windows_x86_64 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Windows_x86_64 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertTrue(platform.IsNativePlatform) self.assertTrue(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertFalse(platform.IsPOSIX) self.assertEqual("\\", platform.PathSeperator) self.assertEqual(";", platform.ValueSeperator) self.assertFalse(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("exe", platform.ExecutableExtension) self.assertEqual("lib", platform.StaticLibraryExtension) self.assertEqual("dll", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Windows (aarch64)" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_NativeWindows_aarch64', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_NativeWindows_aarch64(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Windows_AArch64 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Windows_AArch64 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertTrue(platform.IsNativePlatform) self.assertTrue(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertFalse(platform.IsPOSIX) self.assertEqual("\\", platform.PathSeperator) self.assertEqual(";", platform.ValueSeperator) self.assertFalse(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("exe", platform.ExecutableExtension) self.assertEqual("lib", platform.StaticLibraryExtension) self.assertEqual("dll", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Windows+MSYS2 (x86-64) - MSYS" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_MSYS', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_MSYS(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Windows_MSYS2_MSYS | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Windows_MSYS2_MSYS | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertFalse(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertTrue(platform.IsMSYS2Environment) self.assertTrue(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("exe", platform.ExecutableExtension) self.assertEqual("lib", platform.StaticLibraryExtension) self.assertEqual("dll", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Windows+MSYS2 (x86-64) - MinGW32" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_MinGW32', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_MinGW32(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Windows_MSYS2_MinGW32 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Windows_MSYS2_MinGW32 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertFalse(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertTrue(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertTrue(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("exe", platform.ExecutableExtension) self.assertEqual("lib", platform.StaticLibraryExtension) self.assertEqual("dll", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Windows+MSYS2 (x86-64) - MinGW64" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_MinGW64', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_MinGW64(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Windows_MSYS2_MinGW64 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Windows_MSYS2_MinGW64 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertFalse(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertTrue(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertTrue(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("exe", platform.ExecutableExtension) self.assertEqual("lib", platform.StaticLibraryExtension) self.assertEqual("dll", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Windows+MSYS2 (x86-64) - UCRT64" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_UCRT64', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_UCRT64(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Windows_MSYS2_UCRT64 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Windows_MSYS2_UCRT64 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertFalse(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertTrue(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertTrue(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("exe", platform.ExecutableExtension) self.assertEqual("lib", platform.StaticLibraryExtension) self.assertEqual("dll", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Windows+MSYS2 (x86-64) - Clang32" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_Clang32', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_Clang32(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Windows_MSYS2_Clang32 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Windows_MSYS2_Clang32 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertFalse(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertTrue(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertTrue(platform.IsClang32OnWindows) self.assertFalse(platform.IsClang64OnWindows) self.assertEqual("exe", platform.ExecutableExtension) self.assertEqual("lib", platform.StaticLibraryExtension) self.assertEqual("dll", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) @mark.skipif("Windows+MSYS2 (x86-64) - Clang64" != os_getenv("ENVIRONMENT_NAME", "skip"), reason=f"Skipped 'test_Clang64', if environment variable 'ENVIRONMENT_NAME' doesn't match. {os_getenv('ENVIRONMENT_NAME', 'skip')}") def test_Clang64(self) -> None: platform = Platform() if self.isOnGitHub: self.assertEqual(str(Platforms.Windows_MSYS2_Clang64 | Platforms.CI_GitHubActions), repr(platform)) self.assertTrue(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertTrue(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) else: self.assertEqual(str(Platforms.Windows_MSYS2_Clang64 | Platforms.CI_None), repr(platform)) self.assertFalse(platform.IsCI) self.assertFalse(platform.IsAppVeyor) self.assertFalse(platform.IsGitHub) self.assertFalse(platform.IsGitLab) self.assertFalse(platform.IsTravisCI) self.assertFalse(platform.IsNativeWindows) self.assertFalse(platform.IsNativeLinux) self.assertFalse(platform.IsNativeMacOS) self.assertTrue(platform.IsPOSIX) self.assertEqual("/", platform.PathSeperator) self.assertEqual(":", platform.ValueSeperator) self.assertTrue(platform.IsMSYS2Environment) self.assertFalse(platform.IsMSYSOnWindows) self.assertFalse(platform.IsMinGW32OnWindows) self.assertFalse(platform.IsMinGW64OnWindows) self.assertFalse(platform.IsUCRT64OnWindows) self.assertFalse(platform.IsClang32OnWindows) self.assertTrue(platform.IsClang64OnWindows) self.assertEqual("exe", platform.ExecutableExtension) self.assertEqual("lib", platform.StaticLibraryExtension) self.assertEqual("dll", platform.DynamicLibraryExtension) self.assertNotIn(Platforms.MSYS2_Runtime, platform.HostOperatingSystem) # TODO: FreeBSD pyTooling-8.11.0/tests/unit/TerminalUI/000077500000000000000000000000001513317154500177245ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/TerminalUI/Line.py000066400000000000000000000111461513317154500211700ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ _ _ _ ___ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _|__ _ __ _ __ ___ (_)_ __ __ _| | | | |_ _| # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | | | || | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | __/ | | | | | | | | | | | (_| | | |_| || | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|\___/|___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """pyTooling.TerminalUI""" from unittest import TestCase from pyTooling.TerminalUI import Line, Severity if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Default(self) -> None: line = Line("Message") self.assertEqual("Message", line.Message) self.assertEqual(Severity.Normal, line.Severity) self.assertEqual(0, line.Indent) self.assertEqual("Message", str(line)) def test_Severity(self) -> None: line = Line("Message", Severity.Debug) self.assertEqual(Severity.Debug, line.Severity) def test_Indentation(self) -> None: line = Line("Message", indent=2) self.assertEqual(2, line.Indent) class Indentation(TestCase): def test_IndentationChange(self) -> None: lines = [ Line("Line 1"), Line("Line 1.1", indent=1), Line("Line 2"), ] for line in lines: line.IndentBy(1) self.assertListEqual([1,2,1], [line.Indent for line in lines]) pyTooling-8.11.0/tests/unit/TerminalUI/Severity.py000066400000000000000000000115061513317154500221130ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ _ _ _ ___ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _|__ _ __ _ __ ___ (_)_ __ __ _| | | | |_ _| # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | | | || | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | __/ | | | | | | | | | | | (_| | | |_| || | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|\___/|___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """pyTooling.TerminalUI""" from unittest import TestCase from pyTooling.TerminalUI import Severity if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Comparison(TestCase): def test_Normal(self) -> None: normal = Severity.Normal self.assertLess(Severity.Debug, normal) self.assertLessEqual(Severity.Debug, normal) self.assertEqual(Severity.Normal, normal) self.assertNotEqual(Severity.DryRun, normal) self.assertGreater(Severity.Warning, normal) self.assertGreaterEqual(Severity.Warning, normal) class Exceptions(TestCase): def test_Equal(self) -> None: with self.assertRaises(TypeError): _ = Severity.Normal == 0 def test_Unequal(self) -> None: with self.assertRaises(TypeError): _ = Severity.Normal != 0 def test_Less(self) -> None: with self.assertRaises(TypeError): _ = Severity.Normal < 0 def test_LessOrEqual(self) -> None: with self.assertRaises(TypeError): _ = Severity.Normal <= 0 def test_Greater(self) -> None: with self.assertRaises(TypeError): _ = Severity.Normal > 0 def test_GreaterOrEqual(self) -> None: with self.assertRaises(TypeError): _ = Severity.Normal >= 0 pyTooling-8.11.0/tests/unit/TerminalUI/Terminal.py000066400000000000000000000524431513317154500220610ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ _ _ _ ___ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _|__ _ __ _ __ ___ (_)_ __ __ _| | | | |_ _| # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | | | || | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | __/ | | | | | | | | | | | (_| | | |_| || | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|\___/|___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """pyTooling.TerminalUI""" from io import StringIO from unittest import TestCase from pyTooling.TerminalUI import TerminalApplication, Severity, Mode if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_LineTerminal(self) -> None: term = TerminalApplication() self.assertGreaterEqual(term.Width, 80) self.assertGreaterEqual(term.Height, 25) self.assertFalse(term.Verbose) self.assertFalse(term.Debug) self.assertFalse(term.Quiet) self.assertEqual(Severity.Normal, term.LogLevel) self.assertEqual(0, term.BaseIndent) self.assertEqual(0, len(term.Lines)) self.assertEqual(0, term.ErrorCount) self.assertEqual(0, term.CriticalWarningCount) self.assertEqual(0, term.WarningCount) def test_DerivedApplication(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() self._writeLevel = Severity.Error app = Application() self.assertEqual(Severity.Error, app.LogLevel) def test_ApplicationConfiguration(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app.Configure(verbose=True, debug=True, quiet=True) self.assertTrue(app.Verbose) self.assertTrue(app.Debug) self.assertTrue(app.Quiet) class Properties(TestCase): def test_BaseIndent(self) -> None: term = TerminalApplication() self.assertEqual(0, term.BaseIndent) term.BaseIndent = 2 self.assertEqual(2, term.BaseIndent) def test_LogLevel(self) -> None: term = TerminalApplication() self.assertEqual(Severity.Normal, term.LogLevel) term.LogLevel = Severity.Warning self.assertEqual(Severity.Warning, term.LogLevel) class ExitOnCounters(TestCase): def test_Warnings(self) -> None: print() class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app.ExitOnPreviousWarnings() app.WriteWarning("Message") with self.assertRaises(SystemExit): app.ExitOnPreviousWarnings() def test_Critical(self) -> None: print() class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app.ExitOnPreviousCriticalWarnings() app.WriteCritical("Message") with self.assertRaises(SystemExit): app.ExitOnPreviousCriticalWarnings() def test_Errors(self) -> None: print() class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app.ExitOnPreviousErrors() app.WriteError("Message") with self.assertRaises(SystemExit): app.ExitOnPreviousErrors() class ToStdOut(TestCase): WHITE = "\x1b[97m" YELLOW = "\x1b[93m" DARK_YELLOW = "\x1b[33m" RED = "\x1b[91m" PURPLE = "\x1b[31m" NO_COLOR = "\x1b[39m" def test_WriteFatal(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() try: app.WriteFatal("This is a fatal message.") except SystemExit: pass out.seek(0) err.seek(0) self.assertEqual(f"{self.PURPLE}[FATAL] This is a fatal message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteFatalNoExit(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteFatal("This is a fatal message.", immediateExit=False) out.seek(0) err.seek(0) self.assertEqual(f"{self.PURPLE}[FATAL] This is a fatal message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteError(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteError("This is a error message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.RED}[ERROR] This is a error message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteQuiet(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteQuiet("This is a quiet message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.WHITE}This is a quiet message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteWarning(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteWarning("This is a warning message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.YELLOW}[WARNING] This is a warning message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteCriticalWarning(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteCritical("This is a critical warning message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.DARK_YELLOW}[CRITICAL] This is a critical warning message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteInfo(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteInfo("This is a info message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.WHITE}This is a info message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteNormal(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteNormal("This is a normal message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.WHITE}This is a normal message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteDryRun(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteDryRun("This is a dryRun message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) def test_WriteVerboseDefault(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteVerbose("This is a verbose message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) def test_WriteDebugDefault(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__() app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteDebug("This is a debug message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) class ToStdOut_ToStdErr(TestCase): WHITE = "\x1b[97m" YELLOW = "\x1b[93m" DARK_YELLOW = "\x1b[33m" RED = "\x1b[91m" PURPLE = "\x1b[31m" NO_COLOR = "\x1b[39m" def test_WriteFatal(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() try: app.WriteFatal("This is a fatal message.") except SystemExit: pass out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.PURPLE}[FATAL] This is a fatal message.{self.NO_COLOR}\n", err.readline()) def test_WriteFatalNoExit(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteFatal("This is a fatal message.", immediateExit=False) out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.PURPLE}[FATAL] This is a fatal message.{self.NO_COLOR}\n", err.readline()) def test_WriteError(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteError("This is a error message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.RED}[ERROR] This is a error message.{self.NO_COLOR}\n", err.readline()) def test_WriteQuiet(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteQuiet("This is a quiet message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.WHITE}This is a quiet message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteCriticalWarning(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteCritical("This is a critical warning message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.DARK_YELLOW}[CRITICAL] This is a critical warning message.{self.NO_COLOR}\n", err.readline()) def test_WriteWarning(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteWarning("This is a warning message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.YELLOW}[WARNING] This is a warning message.{self.NO_COLOR}\n", err.readline()) def test_WriteInfo(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteInfo("This is a info message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.WHITE}This is a info message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteNormal(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteNormal("This is a normal message.") out.seek(0) err.seek(0) self.assertEqual(f"{self.WHITE}This is a normal message.{self.NO_COLOR}\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteDryRun(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteDryRun("This is a dryRun message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) def test_WriteVerboseDefault(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteVerbose("This is a verbose message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) def test_WriteDebugDefault(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.TextToStdOut_ErrorsToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteDebug("This is a debug message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) class DataToStdOut(TestCase): WHITE = "\x1b[97m" YELLOW = "\x1b[93m" DARK_YELLOW = "\x1b[33m" RED = "\x1b[91m" PURPLE = "\x1b[31m" NO_COLOR = "\x1b[39m" def test_WriteFatal(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() try: app.WriteFatal("This is a fatal message.") except SystemExit: pass out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.PURPLE}[FATAL] This is a fatal message.{self.NO_COLOR}\n", err.readline()) def test_WriteFatalNoExit(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteFatal("This is a fatal message.", immediateExit=False) out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.PURPLE}[FATAL] This is a fatal message.{self.NO_COLOR}\n", err.readline()) def test_WriteError(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteError("This is a error message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.RED}[ERROR] This is a error message.{self.NO_COLOR}\n", err.readline()) def test_WriteQuiet(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteQuiet("This is a quiet message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.WHITE}This is a quiet message.{self.NO_COLOR}\n", err.readline()) def test_WriteCriticalWarning(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteCritical("This is a critical warning message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.DARK_YELLOW}[CRITICAL] This is a critical warning message.{self.NO_COLOR}\n", err.readline()) def test_WriteWarning(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteWarning("This is a warning message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.YELLOW}[WARNING] This is a warning message.{self.NO_COLOR}\n", err.readline()) def test_WriteInfo(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteInfo("This is a info message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.WHITE}This is a info message.{self.NO_COLOR}\n", err.readline()) def test_WriteNormal(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteNormal("This is a normal message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(f"{self.WHITE}This is a normal message.{self.NO_COLOR}\n", err.readline()) def test_WriteDryRun(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteDryRun("This is a dryRun message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) def test_WriteVerboseDefault(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteVerbose("This is a verbose message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) def test_WriteDebugDefault(self) -> None: class Application(TerminalApplication): def __init__(self) -> None: super().__init__(mode=Mode.DataToStdOut_OtherToStdErr) app = Application() app._stdout, app._stderr = out, err = StringIO(), StringIO() app.WriteDebug("This is a debug message.") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual(0, err.tell()) pyTooling-8.11.0/tests/unit/TerminalUI/TerminalBase.py000066400000000000000000000261231513317154500226500ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ _ _ _ ___ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _|__ _ __ _ __ ___ (_)_ __ __ _| | | | |_ _| # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | | | || | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | __/ | | | | | | | | | | | (_| | | |_| || | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|\___/|___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """pyTooling.TerminalUI""" from io import StringIO from unittest import TestCase from pyTooling.Exceptions import ExceptionBase from pyTooling.TerminalUI import TerminalBaseApplication if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiate(TestCase): def test_NoConfigure(self) -> None: term = TerminalBaseApplication() self.assertGreaterEqual(term.Width, 80) self.assertGreaterEqual(term.Height, 25) def test_UninitializeColors(self) -> None: term = TerminalBaseApplication() term.UninitializeColors() def test_InitializeColors(self) -> None: term = TerminalBaseApplication() term.UninitializeColors() term.InitializeColors() def test_ApplicationDerivedFromTerminal(self) -> None: class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.FATAL_EXIT_CODE = 0 app = Application() class WriteMessages(TestCase): def test_WriteToStdOut(self) -> None: term = TerminalBaseApplication() term._stdout, term._stderr = out, err = StringIO(), StringIO() term.WriteToStdOut("Message") out.seek(0) err.seek(0) self.assertEqual("Message", out.readline()) self.assertEqual(0, err.tell()) def test_WriteLineToStdOut(self) -> None: term = TerminalBaseApplication() term._stdout, term._stderr = out, err = StringIO(), StringIO() term.WriteLineToStdOut("Message") out.seek(0) err.seek(0) self.assertEqual("Message\n", out.readline()) self.assertEqual(0, err.tell()) def test_WriteToStdErr(self) -> None: term = TerminalBaseApplication() term._stdout, term._stderr = out, err = StringIO(), StringIO() term.WriteToStdErr("Message") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual("Message", err.readline()) def test_WriteLineToStdErr(self) -> None: term = TerminalBaseApplication() term._stdout, term._stderr = out, err = StringIO(), StringIO() term.WriteLineToStdErr("Message") out.seek(0) err.seek(0) self.assertEqual(0, out.tell()) self.assertEqual("Message\n", err.readline()) class Exiting(TestCase): def test_Exit(self) -> None: term = TerminalBaseApplication() with self.assertRaises(SystemExit) as ex: term.Exit(1) self.assertEqual(1, ex.exception.code) def test_FatalExit4(self) -> None: class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() app = Application() with self.assertRaises(SystemExit) as ex: app.FatalExit(4) self.assertEqual(4, ex.exception.code) def test_FatalExit254(self) -> None: class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.FATAL_EXIT_CODE = 254 app = Application() with self.assertRaises(SystemExit) as ex: app.FatalExit() self.assertEqual(254, ex.exception.code) def test_FatalExitDefault(self) -> None: class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() app = Application() with self.assertRaises(SystemExit) as ex: app.FatalExit() self.assertEqual(255, ex.exception.code) def test_CheckPythonVersion3(self) -> None: class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() super().CheckPythonVersion((3, 8, 0)) _ = Application() def test_CheckPythonVersion4(self) -> None: print() class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() super().CheckPythonVersion((4, 9, 0)) with self.assertRaises(SystemExit) as exitEx: _ = Application() self.assertEqual(254, exitEx.exception.code) class ExceptionHandling(TestCase): def test_NotImplemented(self) -> None: print() class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.ISSUE_TRACKER_URL = "https://GitHub.com/pyTooling/pyTooling/issues" def Run(self): raise NotImplementedError(f"Abstract method") app = Application() try: app.Run() except NotImplementedError as ex: with self.assertRaises(SystemExit) as exitEx: app.PrintNotImplementedError(ex) self.assertEqual(240, exitEx.exception.code) def test_Exception(self) -> None: print() class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.ISSUE_TRACKER_URL = "https://GitHub.com/pyTooling/pyTooling/issues" def Run(self): raise Exception(f"Common exception") app = Application() try: app.Run() except Exception as ex: with self.assertRaises(SystemExit) as exitEx: app.PrintException(ex) self.assertEqual(241, exitEx.exception.code) def test_ExceptionWithNote(self) -> None: print() class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.ISSUE_TRACKER_URL = "https://GitHub.com/pyTooling/pyTooling/issues" def Run(self): ex = Exception(f"Common exception") ex.add_note("First note") raise ex app = Application() try: app.Run() except Exception as ex: with self.assertRaises(SystemExit) as exitEx: app.PrintException(ex) self.assertEqual(241, exitEx.exception.code) def test_ExceptionWithNotes(self) -> None: print() class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.ISSUE_TRACKER_URL = "https://GitHub.com/pyTooling/pyTooling/issues" def Run(self): ex = Exception(f"Common exception") ex.add_note("First note") ex.add_note("Second note") raise ex app = Application() try: app.Run() except Exception as ex: with self.assertRaises(SystemExit) as exitEx: app.PrintException(ex) self.assertEqual(241, exitEx.exception.code) def test_ExceptionWithNestedException(self) -> None: print() class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.ISSUE_TRACKER_URL = "https://GitHub.com/pyTooling/pyTooling/issues" def Run(self): ex = Exception(f"Common exception") ex.add_note("First note") nex = FileNotFoundError(f"File doesn't exist.") nex.add_note("Nested note") nex.add_note("Second nested line") raise ex from nex app = Application() try: app.Run() except Exception as ex: with self.assertRaises(SystemExit) as exitEx: app.PrintException(ex) self.assertEqual(241, exitEx.exception.code) def test_ExceptionBase(self) -> None: print() class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.ISSUE_TRACKER_URL = "https://GitHub.com/pyTooling/pyTooling/issues" def Run(self): raise ExceptionBase(f"Base exception") app = Application() try: app.Run() except ExceptionBase as ex: with self.assertRaises(SystemExit) as exitEx: app.PrintExceptionBase(ex) self.assertEqual(241, exitEx.exception.code) def test_ExceptionBaseWithNestedException(self) -> None: print() class Application(TerminalBaseApplication): def __init__(self) -> None: super().__init__() self.__class__.ISSUE_TRACKER_URL = "https://GitHub.com/pyTooling/pyTooling/issues" def Run(self): raise ExceptionBase(f"Base exception") from FileNotFoundError(f"File doesn't exist.") app = Application() try: app.Run() except ExceptionBase as ex: with self.assertRaises(SystemExit) as exitEx: app.PrintExceptionBase(ex) self.assertEqual(241, exitEx.exception.code) pyTooling-8.11.0/tests/unit/TerminalUI/__init__.py000066400000000000000000000066701513317154500220460ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ _ _ _ ___ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _|__ _ __ _ __ ___ (_)_ __ __ _| | | | |_ _| # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | | | || | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | __/ | | | | | | | | | | | (_| | | |_| || | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|\___/|___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # pyTooling-8.11.0/tests/unit/Tracing/000077500000000000000000000000001513317154500173025ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Tracing/__init__.py000066400000000000000000000250731513317154500214220ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ __ _ ___(_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _` |/ __| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | (_| | (__| | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \__,_|\___|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for :mod:`pyTooling.Tracing`.""" from colorama import Fore from time import sleep from unittest import TestCase from pyTooling.Tracing import TracingException, Trace, Span, Event if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Trace(self) -> None: t = Trace("trace") self.assertIsNone(t.Parent) self.assertEqual("trace", t.Name) self.assertEqual("trace", str(t)) self.assertFalse(t.HasSubSpans) self.assertEqual(0, t.SubSpanCount) self.assertEqual(0, len([s for s in t.IterateSubSpans()])) self.assertFalse(t.HasEvents) self.assertEqual(0, t.EventCount) self.assertEqual(0, len([e for e in t.IterateEvents()])) self.assertEqual(0, len(t)) self.assertEqual(0, len([a for a in t])) with self.assertRaises(TracingException) as ex: _ = t.Duration def test_Span(self) -> None: s = Span("span") self.assertIsNone(s.Parent) self.assertEqual("span", s.Name) self.assertEqual("span", str(s)) self.assertFalse(s.HasSubSpans) self.assertEqual(0, s.SubSpanCount) self.assertEqual(0, len([ss for ss in s.IterateSubSpans()])) self.assertFalse(s.HasEvents) self.assertEqual(0, s.EventCount) self.assertEqual(0, len([e for e in s.IterateEvents()])) self.assertEqual(0, len(s)) self.assertEqual(0, len([a for a in s])) def test_SubSpan(self) -> None: s = Span("span") ss = Span("subspan", parent=s) self.assertIsNone(s.Parent) self.assertTrue(s.HasSubSpans) self.assertEqual(1, s.SubSpanCount) self.assertListEqual([ss], [ss for ss in s.IterateSubSpans()]) self.assertIs(s, ss.Parent) self.assertFalse(ss.HasSubSpans) self.assertEqual(0, ss.SubSpanCount) def test_Event(self) -> None: e = Event("event") self.assertIsNone(e.Parent) self.assertEqual("event", e.Name) self.assertEqual("event", str(e)) self.assertEqual(0, len(e)) self.assertEqual(0, len([a for a in e])) class Context(TestCase): def test_Trace(self) -> None: print() self.assertIsNone(Trace.CurrentTrace()) self.assertIsNone(Trace.CurrentSpan()) with Trace("trace") as t: self.assertIs(Trace.CurrentTrace(), t) self.assertIs(Trace.CurrentSpan(), t) sleep(0.001) self.assertIsNone(Trace.CurrentTrace()) self.assertIsNone(Trace.CurrentSpan()) self.assertIsNone(t.Parent) self.assertFalse(t.HasSubSpans) self.assertEqual(0, t.SubSpanCount) self.assertEqual(0, len([s for s in t.IterateSubSpans()])) self.assertEqual(0, t.EventCount) self.assertEqual(0, len([e for e in t.IterateEvents()])) self.assertEqual(0, len(t)) self.assertEqual(0, len([a for a in t])) print(f"Duration: {t.Duration*1e3:.3f} ms") for line in t.Format(): print(line) def test_Span(self) -> None: print() self.assertIsNone(Trace.CurrentTrace()) with Trace("trace") as t: sleep(0.001) with Span("span") as s: sleep(0.001) sleep(0.001) self.assertIsNone(t.Parent) self.assertTrue(t.HasSubSpans) self.assertEqual(1, t.SubSpanCount) self.assertEqual(1, len([s for s in t.IterateSubSpans()])) self.assertEqual(0, t.EventCount) self.assertEqual(0, len([e for e in t.IterateEvents()])) self.assertEqual(0, len(t)) self.assertEqual(0, len([a for a in t])) self.assertIs(t, s.Parent) self.assertFalse(s.HasSubSpans) self.assertEqual(0, s.SubSpanCount) for line in t.Format(): print(line) def test_Event(self) -> None: print() self.assertIsNone(Trace.CurrentTrace()) with Trace("trace") as t: sleep(0.001) with Span("span") as s: sleep(0.001) e = Event("event", parent=s) sleep(0.001) self.assertIsNone(t.Parent) self.assertTrue(t.HasSubSpans) self.assertEqual(1, t.SubSpanCount) self.assertEqual(1, len([s for s in t.IterateSubSpans()])) self.assertEqual(0, t.EventCount) self.assertEqual(0, len([e for e in t.IterateEvents()])) self.assertEqual(0, len(t)) self.assertEqual(0, len([a for a in t])) self.assertIs(t, s.Parent) self.assertFalse(s.HasSubSpans) self.assertEqual(0, s.SubSpanCount) for line in t.Format(): print(line) def test_Spans(self) -> None: print() self.assertIsNone(Trace.CurrentTrace()) with Trace("trace") as t: sleep(0.001) with Span("span 1") as s: sleep(0.001) with Span("span 1.1") as s: sleep(0.001) with Span("span 1.1.1") as s: sleep(0.001) with Span("span 1.2") as s: sleep(0.001) with Span("span 1.3") as s: sleep(0.001) with Span("span 1.3.1") as s: sleep(0.001) with Span("span 1.4") as s: sleep(0.001) with Span("span 2") as s: sleep(0.001) with Span("span 3") as s: sleep(0.001) with Span("span 3.1") as s: sleep(0.001) with Span("span 3.1.1") as s: sleep(0.001) with Span("span 3.1.2") as s: sleep(0.001) with Span("span 4") as s: sleep(0.001) sleep(0.001) self.assertEqual(4, t.SubSpanCount) self.assertEqual(4, len([s for s in t.IterateSubSpans()])) for line in t.Format(): print(line) class Attributes(TestCase): def test_Trace(self) -> None: t = Trace("trace") self.assertEqual(0, len(t)) self.assertEqual(0, len([a for a in t])) t["id1"] = "value" self.assertEqual(1, len(t)) self.assertEqual(1, len([a for a in t])) self.assertIn("id1", t) self.assertEqual("value", t["id1"]) t["id1"] = "value1" self.assertEqual(1, len(t)) self.assertListEqual([("id1", "value1")], [a for a in t]) t["id2"] = "value2" self.assertEqual(2, len(t)) self.assertIn("id2", t) self.assertListEqual([("id1", "value1"), ("id2", "value2")], [a for a in t]) del t["id1"] self.assertEqual(1, len(t)) self.assertListEqual([("id2", "value2")], [a for a in t]) self.assertIn("id2", t) def test_Span(self) -> None: s = Span("span") self.assertEqual(0, len(s)) self.assertEqual(0, len([a for a in s])) s["id1"] = "value" self.assertEqual(1, len(s)) self.assertEqual(1, len([a for a in s])) self.assertIn("id1", s) self.assertEqual("value", s["id1"]) s["id1"] = "value1" self.assertEqual(1, len(s)) self.assertListEqual([("id1", "value1")], [a for a in s]) s["id2"] = "value2" self.assertEqual(2, len(s)) self.assertIn("id2", s) self.assertListEqual([("id1", "value1"), ("id2", "value2")], [a for a in s]) del s["id1"] self.assertEqual(1, len(s)) self.assertListEqual([("id2", "value2")], [a for a in s]) self.assertIn("id2", s) def test_Event(self) -> None: e = Event("event") self.assertEqual(0, len(e)) self.assertEqual(0, len([a for a in e])) e["id1"] = "value" self.assertEqual(1, len(e)) self.assertEqual(1, len([a for a in e])) self.assertIn("id1", e) self.assertEqual("value", e["id1"]) e["id1"] = "value1" self.assertEqual(1, len(e)) self.assertListEqual([("id1", "value1")], [a for a in e]) e["id2"] = "value2" self.assertEqual(2, len(e)) self.assertIn("id2", e) self.assertListEqual([("id1", "value1"), ("id2", "value2")], [a for a in e]) del e["id1"] self.assertEqual(1, len(e)) self.assertListEqual([("id2", "value2")], [a for a in e]) self.assertIn("id2", e) pyTooling-8.11.0/tests/unit/Tree/000077500000000000000000000000001513317154500166125ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Tree/__init__.py000066400000000000000000000615371513317154500207370ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ _____ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ ___ ___ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _ \/ _ \ # # | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | __/ __/ # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \___|\___| # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for pyTooling.Tree.""" from typing import Any, Optional as Nullable, List, Tuple, Dict from unittest import TestCase from pytest import mark from pyTooling.Tree import Node, AlreadyInTreeError, NoSiblingsError if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Construction(TestCase): def test_SingleNode(self) -> None: root: Node[Nullable[Any], int, str, Any] = Node() self.assertIs(root, root.Root) self.assertIsNone(root.Parent) self.assertIsNone(root.ID) self.assertEqual(0, root.Level) self.assertEqual(0, len(root)) self.assertTrue(root.IsRoot) self.assertTrue(root.IsLeaf) def test_NewNodeWithParent(self) -> None: root = Node() children = [Node(parent=root), Node(parent=root)] self.assertIs(root, root.Root) self.assertIsNone(root.ID) self.assertIsNone(root.Parent) self.assertEqual(0, root.Level) self.assertTrue(root.IsRoot) self.assertFalse(root.IsLeaf) self.assertTrue(root.HasChildren) for child in children: self.assertIs(root, child.Root) self.assertIsNone(child.ID) self.assertEqual(1, child.Level) self.assertFalse(child.IsRoot) self.assertTrue(child.IsLeaf) self.assertFalse(child.HasChildren) self.assertListEqual([root, child], list(child.Path)) self.assertListEqual([root, child], list(child.GetPath())) self.assertListEqual([root], [ancestor for ancestor in child.GetAncestors()]) self.assertListEqual(children, [child for child in root.GetChildren()]) # self.assertListEqual(children, [child for child in root.GetSiblings()]) def test_NewNodeWithChildren(self) -> None: children = [Node(), Node()] root = Node(children=children) self.assertIs(root, root.Root) self.assertIsNone(root.ID) self.assertIsNone(root.Parent) self.assertEqual(0, root.Level) self.assertTrue(root.IsRoot) self.assertFalse(root.IsLeaf) self.assertTrue(root.HasChildren) for child in children: self.assertIs(root, child.Root) self.assertIsNone(child.ID) self.assertEqual(1, child.Level) self.assertFalse(child.IsRoot) self.assertTrue(child.IsLeaf) self.assertFalse(child.HasChildren) self.assertListEqual([root, child], list(child.Path)) self.assertListEqual([root, child], list(child.GetPath())) self.assertListEqual([root], [ancestor for ancestor in child.GetAncestors()]) self.assertListEqual(children, [child for child in root.GetChildren()]) self.assertListEqual(children, [child for child in root.GetDescendants()]) def test_GrandChildren(self) -> None: root = Node(1) children = [Node(2, parent=root), Node(3, parent=root)] grandChildren = [Node(4, parent=children[0]), Node(5, parent=children[0]), Node(6, parent=children[1]), Node(7, parent=children[1])] self.assertIs(root, root.Root) self.assertIsNone(root.Parent) self.assertEqual(0, root.Level) self.assertTrue(root.IsRoot) self.assertFalse(root.IsLeaf) self.assertTrue(root.HasChildren) for child in children: self.assertIs(root, child.Root) self.assertEqual(1, child.Level) self.assertFalse(child.IsRoot) self.assertFalse(child.IsLeaf) self.assertTrue(child.HasChildren) self.assertListEqual([root, child], list(child.Path)) self.assertListEqual([root, child], list(child.GetPath())) self.assertListEqual([root], [ancestor for ancestor in child.GetAncestors()]) self.assertListEqual(children, [child for child in root.GetChildren()]) for grandChild in grandChildren: self.assertIs(root, grandChild.Root) self.assertEqual(2, grandChild.Level) self.assertFalse(grandChild.IsRoot) self.assertTrue(grandChild.IsLeaf) self.assertFalse(grandChild.HasChildren) self.assertListEqual([root, grandChild.Parent, grandChild], list(grandChild.Path)) self.assertListEqual([root, grandChild.Parent, grandChild], list(grandChild.GetPath())) self.assertListEqual([grandChild.Parent, root], [ancestor for ancestor in grandChild.GetAncestors()]) self.assertListEqual( [children[0], grandChildren[0], grandChildren[1], children[1], grandChildren[2], grandChildren[3]], [child for child in root.GetDescendants()] ) def test_AddChild(self) -> None: root = Node(1) child = Node(2) root.AddChild(child) self.assertIs(root, root.Root) self.assertEqual(0, root.Level) self.assertTrue(root.IsRoot) self.assertTrue(root.HasChildren) self.assertFalse(root.IsLeaf) self.assertListEqual([child], [child for child in root.GetChildren()]) self.assertIs(root, child.Root) self.assertEqual(1, child.Level) self.assertFalse(child.IsRoot) self.assertTrue(child.IsLeaf) self.assertFalse(child.HasChildren) self.assertIs(root, child.Parent) def test_AddChildTree(self) -> None: root = Node(1) child = Node(2) grandChild = Node(3, parent=child) root.AddChild(child) self.assertIs(root, root.Root) self.assertEqual(0, root.Level) self.assertTrue(root.IsRoot) self.assertTrue(root.HasChildren) self.assertFalse(root.IsLeaf) self.assertListEqual([child], [child for child in root.GetChildren()]) self.assertIs(root, child.Root) self.assertEqual(1, child.Level) self.assertFalse(child.IsRoot) self.assertFalse(child.IsLeaf) self.assertTrue(child.HasChildren) self.assertIs(root, child.Parent) self.assertIs(root, grandChild.Root) self.assertEqual(2, grandChild.Level) self.assertFalse(grandChild.IsRoot) self.assertTrue(grandChild.IsLeaf) self.assertFalse(grandChild.HasChildren) self.assertIs(child, grandChild.Parent) def test_AddChildren(self) -> None: root = Node(1) children = [Node(11), Node(12)] grandChildren = [ Node(111, parent=children[0]), Node(112, parent=children[0]), Node(121, parent=children[1]), Node(122, parent=children[1]) ] root.AddChildren(children) self.assertIs(root, root.Root) self.assertEqual(0, root.Level) self.assertTrue(root.IsRoot) self.assertTrue(root.HasChildren) self.assertFalse(root.IsLeaf) self.assertListEqual(children, [child for child in root.GetChildren()]) for child in children: self.assertIs(root, child.Root) self.assertEqual(1, child.Level) self.assertFalse(child.IsRoot) self.assertFalse(child.IsLeaf) self.assertTrue(child.HasChildren) self.assertIs(root, child.Parent) for grandChild in grandChildren: self.assertIs(root, grandChild.Root) self.assertEqual(2, grandChild.Level) self.assertFalse(grandChild.IsRoot) self.assertTrue(grandChild.IsLeaf) self.assertFalse(grandChild.HasChildren) def test_SetParent(self) -> None: root = Node(1) child = Node(2) child.Parent = root self.assertIs(root, root.Root) self.assertEqual(0, root.Level) self.assertTrue(root.IsRoot) self.assertTrue(root.HasChildren) self.assertFalse(root.IsLeaf) self.assertListEqual([child], [child for child in root.GetChildren()]) self.assertIs(root, child.Root) self.assertEqual(1, child.Level) self.assertFalse(child.IsRoot) self.assertTrue(child.IsLeaf) self.assertFalse(child.HasChildren) self.assertIs(root, child.Parent) class MergeTree(TestCase): def test_SetParent(self) -> None: root = Node(1) children = [Node(2, parent=root), Node(3, parent=root)] root1 = Node(11) children1 = [Node(12, parent=root1), Node(13, parent=root1)] root2 = Node(21) children2 = [Node(22, parent=root2), Node(23, parent=root2)] root1.Parent = children[0] root2.Parent = children[1] nodes = [root1, root2] + children1 + children2 for child in children: self.assertFalse(child.IsLeaf) for node in nodes: self.assertFalse(node.IsRoot) self.assertIs(root, node.Root) # check if sub trees IDs are now in main tree def test_AddChild(self) -> None: root = Node(1) children = [Node(2, parent=root), Node(3, parent=root)] root1 = Node(11) children1 = [Node(12, parent=root1), Node(13, parent=root1)] root2 = Node(21) children2 = [Node(22, parent=root2), Node(23, parent=root2)] children[0].AddChild(root1) children[1].AddChild(root2) nodes = [root1, root2] + children1 + children2 for child in children: self.assertFalse(child.IsLeaf) for node in nodes: self.assertFalse(node.IsRoot) self.assertIs(root, node.Root) @mark.skip(reason="Not yet implemented!") def test_AddChildren(self) -> None: pass class SplitTree(TestCase): def test_SplitTreeWithoutIDs(self) -> None: root = Node() children = [Node(parent=root), Node(parent=root)] root1 = Node(parent=children[0]) children1 = [Node(parent=root1), Node(parent=root1)] oldSize = root.Size root1.Parent = None self.assertTrue(children[0].IsLeaf) self.assertEqual(0, len(children[0])) self.assertEqual(oldSize, root.Size + root1.Size) self.assertEqual(len(children), len(root)) self.assertEqual(len(children1), len(root1)) self.assertIsNone(root1.Parent) self.assertTrue(root1.IsRoot) self.assertIs(root1, root1.Root) self.assertEqual(0, root1.Level) for node in children1: self.assertIs(root1, node.Root) self.assertEqual(1, node.Level) def test_SplitTreeWithIDs(self) -> None: root = Node(1) children = [Node(2, parent=root), Node(3, parent=root)] root1 = Node(11, parent=children[0]) children1 = [Node(12, parent=root1), Node(13, parent=root1)] oldSize = root.Size root1.Parent = None self.assertTrue(children[0].IsLeaf) self.assertEqual(0, len(children[0])) self.assertEqual(oldSize, root.Size + root1.Size) self.assertEqual(len(children), len(root)) self.assertEqual(len(children1), len(root1)) self.assertIsNone(root1.Parent) self.assertTrue(root1.IsRoot) self.assertIs(root1, root1.Root) self.assertEqual(0, root1.Level) for node in children1: self.assertIs(root1, node.Root) self.assertEqual(1, node.Level) # check if subtree's IDs are not in main tree anymore @mark.skip(reason="Not yet implemented!") def test_DeleteChild(self) -> None: pass class Loops(TestCase): def test_SelfLoop(self) -> None: root = Node(1) with self.assertRaises(Exception): root.AddChild(root) with self.assertRaises(Exception): root.Parent = root def test_MinimalLoop(self) -> None: root = Node(1) child = Node(2, parent=root) with self.assertRaises(Exception): child.AddChild(root) with self.assertRaises(Exception): root.Parent = child def test_InternalLoop(self) -> None: root = Node(1) child = Node(2, parent=root) grandchild = Node(3, parent=child) with self.assertRaises(Exception): grandchild.AddChild(root) with self.assertRaises(Exception): grandchild.AddChild(child) with self.assertRaises(Exception): root.Parent = grandchild with self.assertRaises(Exception): child.Parent = grandchild def test_SideLoop(self) -> None: root = Node(1) child = Node(2, parent=root) grandchild = [Node(3, parent=child), Node(4, parent=child)] with self.assertRaises(Exception): grandchild[1].AddChild(grandchild[0]) with self.assertRaises(Exception): grandchild[0].Parent = grandchild[1] class Features(TestCase): def test_NodeWithID(self) -> None: root = Node(nodeID=1) self.assertIs(1, root.ID) self.assertIs(root, root.GetNodeByID(1)) with self.assertRaises(AttributeError): root.ID = "2" def test_NodeWithValue(self) -> None: root = Node(value="1") self.assertIs("1", root.Value) root.Value = "2" self.assertIs("2", root.Value) def test_NodeWithDictionary(self) -> None: root = Node() # set value root["key1"] = "value1" # get value self.assertIs("value1", root["key1"]) # del value del root["key1"] with self.assertRaises(KeyError): _ = root["key1"] def test_Length(self) -> None: root = Node(1) children = [Node(2, parent=root), Node(3, parent=root)] self.assertEqual(len(children), len(root)) def test_Size(self) -> None: root = Node(1) children = [Node(2, parent=root), Node(3, parent=root)] grandChildren = [ Node(4, parent=children[0]), Node(5, parent=children[0]), Node(6, parent=children[1]), Node(7, parent=children[1]) ] grandGrandChildren = [ Node(8, parent=grandChildren[0]), Node(9, parent=grandChildren[1]), Node(10, parent=grandChildren[1]), Node(11, parent=grandChildren[3]) ] size = 1 + len(children) + len(grandChildren) + len(grandGrandChildren) self.assertEqual(size, root.Size) def test_Iterator(self) -> None: root = Node(1) children = [Node(2, parent=root), Node(3, parent=root)] self.assertListEqual(children, [node for node in root]) def test_Siblings(self) -> None: root = Node(1) children = [ Node(11, parent=root), Node(12, parent=root), Node(13, parent=root), Node(14, parent=root), Node(15, parent=root) ] self.assertListEqual(children[:2] + children[3:], list(children[2].Siblings)) def test_LeftSiblings(self) -> None: root = Node(1) children = [ Node(11, parent=root), Node(12, parent=root), Node(13, parent=root), Node(14, parent=root), Node(15, parent=root) ] self.assertListEqual(children[:2], list(children[2].LeftSiblings)) def test_RightSiblings(self) -> None: root = Node(1) children = [ Node(11, parent=root), Node(12, parent=root), Node(13, parent=root), Node(14, parent=root), Node(15, parent=root) ] self.assertListEqual(children[3:], list(children[2].RightSiblings)) def test_Repr(self) -> None: node = Node() nodeID = Node(nodeID=2) nodeValue = Node(value="2") nodeIDValue = Node(nodeID=2, value="2") _ = repr(node) _ = repr(nodeID) _ = repr(nodeValue) _ = repr(nodeIDValue) root = Node() node = Node(parent=root) nodeID = Node(nodeID=31, parent=root) nodeValue = Node(value="32", parent=root) nodeIDValue = Node(nodeID=33, value="33", parent=root) _ = repr(node) _ = repr(nodeID) _ = repr(nodeValue) _ = repr(nodeIDValue) root = Node(1) node = Node(parent=root) nodeID = Node(nodeID=41, parent=root) nodeValue = Node(value="42", parent=root) nodeIDValue = Node(nodeID=43, value="43", parent=root) _ = repr(node) _ = repr(nodeID) _ = repr(nodeValue) _ = repr(nodeIDValue) def test_Str(self) -> None: node = Node() nodeID = Node(nodeID=1) nodeValue = Node(value="1") nodeIDValue = Node(nodeID=1, value="1") _ = str(node) _ = str(nodeID) _ = str(nodeValue) _ = str(nodeIDValue) class Iteration(TestCase): _root: Node _children: List[Node] def setUp(self) -> None: root = Node(1) children = [ Node(11, parent=root), Node(12, parent=root), Node(13, parent=root), Node(14, parent=root), Node(15, parent=root) ] grandChildren = [ Node(111, parent=children[0]), Node(112, parent=children[0]), Node(121, parent=children[1]), Node(131, parent=children[2]), Node(132, parent=children[2]), Node(133, parent=children[2]), Node(141, parent=children[3]), Node(142, parent=children[3]), Node(151, parent=children[4]) ] grandGrandChildren = [ Node(1111, parent=grandChildren[0]), Node(1121, parent=grandChildren[1]), Node(1122, parent=grandChildren[1]), Node(1311, parent=grandChildren[3]), Node(1312, parent=grandChildren[3]), Node(1321, parent=grandChildren[4]), Node(1331, parent=grandChildren[5]), Node(1421, parent=grandChildren[7]), Node(1422, parent=grandChildren[7]), Node(1511, parent=grandChildren[8]) ] grandGrandGrandChildren = [ Node(13211, parent=grandGrandChildren[5]), Node(13212, parent=grandGrandChildren[5]), Node(14221, parent=grandGrandChildren[8]), Node(14222, parent=grandGrandChildren[8]) ] self._root = root self._children = children def test_Siblings(self) -> None: self.assertListEqual([11, 12, 14, 15], [node.ID for node in self._children[2].Siblings]) def test_LeftSiblings(self) -> None: self.assertListEqual([11, 12], [node.ID for node in self._children[2].LeftSiblings]) def test_RightSiblings(self) -> None: self.assertListEqual([14, 15], [node.ID for node in self._children[2].RightSiblings]) def test_GetSiblings(self) -> None: self.assertListEqual([11, 12, 14, 15], [node.ID for node in self._children[2].GetSiblings()]) def test_GetLeftSiblings(self) -> None: self.assertListEqual([11, 12], [node.ID for node in self._children[2].GetLeftSiblings()]) def test_GetRightSiblings(self) -> None: self.assertListEqual([14, 15], [node.ID for node in self._children[2].GetRightSiblings()]) def test_GetLeftRelatives(self) -> None: self.assertListEqual([ 11, 111, 1111, 112, 1121, 1122, 12, 121, ], [node.ID for node in self._children[2].GetLeftRelatives()]) def test_GetRightRelatives(self) -> None: self.assertListEqual([ 14, 141, 142, 1421, 1422, 14221, 14222, 15, 151, 1511 ], [node.ID for node in self._children[2].GetRightRelatives()]) def test_IteratePreOrder(self) -> None: self.assertListEqual([ 1, 11, 111, 1111, 112, 1121, 1122, 12, 121, 13, 131, 1311, 1312, 132, 1321, 13211, 13212, 133, 1331, 14, 141, 142, 1421, 1422, 14221, 14222, 15, 151, 1511 ], [node.ID for node in self._root.IteratePreOrder()]) def test_IteratePostOrder(self) -> None: self.assertListEqual([ 1111, 111, 1121, 1122, 112, 11, 121, 12, 1311, 1312, 131, 13211, 13212, 1321, 132, 1331, 133, 13, 141, 1421, 14221, 14222, 1422, 142, 14, 1511, 151, 15, 1 ], [node.ID for node in self._root.IteratePostOrder()]) def test_IterateLevelOrder(self) -> None: self.assertListEqual([ 1, 11, 12, 13, 14, 15, 111, 112, 121, 131, 132, 133, 141, 142, 151, 1111, 1121, 1122, 1311, 1312, 1321, 1331, 1421, 1422, 1511, 13211, 13212, 14221, 14222, ], [node.ID for node in self._root.IterateLevelOrder()]) def test_IterateLeafs(self) -> None: self.assertListEqual([ 1111, 1121, 1122, 121, 1311, 1312, 13211, 13212, 1331, 141, 1421, 14221, 14222, 1511 ], [node.ID for node in self._root.IterateLeafs()]) class Exceptions(TestCase): def test_NewNodeWithWrongParent(self) -> None: with self.assertRaises(TypeError): _ = Node(parent=1) def test_NewNodeWithDuplicateID(self) -> None: root = Node(1) with self.assertRaises(ValueError): _ = Node(1, parent=root) def test_NewNodeWithNonIterableChildren(self) -> None: with self.assertRaises(TypeError): _ = Node(children=0) def test_NewNodeWithWrongChildInChildren(self) -> None: children = [Node(), None, Node()] with self.assertRaises(TypeError): _ = Node(children=children) def test_SetWrongParent(self) -> None: root = Node() child = Node(parent=root) with self.assertRaises(TypeError): child.Parent = 2 def test_AddWrongChild(self) -> None: root = Node() with self.assertRaises(TypeError): root.AddChild(2) def test_AddWrongChildInChildren(self) -> None: root = Node() children = [Node(), None, Node()] with self.assertRaises(TypeError): root.AddChildren(children) def test_AddExistingChildInChildren(self) -> None: root = Node() children = [Node(), Node(parent=root), Node()] with self.assertRaises(AlreadyInTreeError): root.AddChildren(children) def test_SetParentWithDuplicateIDs(self) -> None: root = Node(1) root1 = Node(1) with self.assertRaises(ValueError): root1.Parent = root def test_GetNodeByIDNone(self) -> None: root = Node(1) with self.assertRaises(ValueError): root.GetNodeByID(None) def test_GetSiblingsOfRoot(self) -> None: root = Node(1) self.assertIsNone(root.Parent) with self.assertRaises(NoSiblingsError): _ = root.Siblings with self.assertRaises(NoSiblingsError): for _ in root.GetSiblings(): pass def test_GetLeftSiblingsOfRoot(self) -> None: root = Node(1) self.assertIsNone(root.Parent) with self.assertRaises(NoSiblingsError): _ = root.LeftSiblings with self.assertRaises(NoSiblingsError): for _ in root.GetLeftSiblings(): pass def test_GetRightSiblingsOfRoot(self) -> None: root = Node(1) self.assertIsNone(root.Parent) with self.assertRaises(NoSiblingsError): _ = root.RightSiblings with self.assertRaises(NoSiblingsError): for _ in root.GetRightSiblings(): pass class Rendering(TestCase): # parentID, nodeID, dict _tree: Tuple[Tuple[int, int, Dict[str, float]], ...] = ( (0, 1, {"time": 11.0}), (0, 2, {"time": 3.2}), (0, 3, {"time": 7.9}), (1, 4, {"time": 29.2}), (1, 5, {"time": 31.4}), # 2 (3, 6, {"time": 4.1}), (3, 7, {"time": 5.6}), (4, 8, {"time": 19.7}), (5, 10, {"time": 0.6}), # 6 # 7 (8, 9, {"time": 1.1}), # 9 (10, 11, {"time": 25.8}), (10, 12, {"time": 14.0}), (10, 13, {"time": 17.5}), # 11 # 12 # 13 ) def test_RenderDefault(self) -> None: print() root = Node(nodeID=0, value="", keyValuePairs={"time": 85.3}) for parentID, childID, kvp in self._tree: Node(nodeID=childID, value=f"", keyValuePairs=kvp, parent=root.GetNodeByID(parentID)) print("=" * 40) rendering = root.Render() print(rendering, end="") print("=" * 40) rendering = root.Render(bypassMarker=" │ ", nodeMarker=" ├──", lastNodeMarker=" └──") print(rendering, end="") print("=" * 40) rendering = root.Render(bypassMarker="| ", nodeMarker="o-- ", lastNodeMarker="`-- ") print(rendering, end="") print("=" * 40) self.assertEqual(len(self._tree) + 2, len(rendering.split("\n"))) def test_RenderUserdefined(self) -> None: print() def format(node: Node) -> str: return f"{node._id}: {node._value} - {','.join(f'{k}:{v}' for k, v in node._dict.items())}" root = Node(nodeID=0, value="", keyValuePairs={"time": 85.3}, format=format) for parentID, childID, kvp in self._tree: Node(nodeID=childID, value=f"", keyValuePairs=kvp, parent=root.GetNodeByID(parentID), format=format) print("=" * 40) rendering = root.Render() print(rendering, end="") print("=" * 40) self.assertEqual(len(self._tree) + 2, len(rendering.split("\n"))) pyTooling-8.11.0/tests/unit/Versioning/000077500000000000000000000000001513317154500200365ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Versioning/CalVersion.py000066400000000000000000000432341513317154500224630ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2020-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for package :mod:`pyTooling.Versioning`.""" from unittest import TestCase from pytest import mark from pyTooling.Versioning import Flags, CalendarVersion, WordSizeValidator, MaxValueValidator from pyTooling.Versioning import YearMonthVersion, YearWeekVersion, YearReleaseVersion, YearMonthDayVersion if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Major(self) -> None: version = CalendarVersion(1) self.assertEqual(1, version.Major) self.assertEqual(0, version.Minor) self.assertEqual(Flags.Clean, version.Flags) def test_MajorMinor(self) -> None: version = CalendarVersion(1, 2) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) def test_Major_String(self) -> None: with self.assertRaises(TypeError): _ = CalendarVersion("1") def test_Major_Negative(self) -> None: with self.assertRaises(ValueError): _ = CalendarVersion(-1) def test_Major_Minor_String(self) -> None: with self.assertRaises(TypeError): _ = CalendarVersion(1, "2") def test_Major_Minor_Negative(self) -> None: with self.assertRaises(ValueError): _ = CalendarVersion(1, -2) class Parsing(TestCase): def test_None(self) -> None: with self.assertRaises(ValueError): CalendarVersion.Parse(None) def test_EmptyString(self) -> None: with self.assertRaises(ValueError): CalendarVersion.Parse("") def test_OtherType(self) -> None: with self.assertRaises(TypeError): CalendarVersion.Parse(1) def test_InvalidString(self) -> None: with self.assertRaises(ValueError): CalendarVersion.Parse("None") def test_String_Major(self) -> None: version = CalendarVersion.Parse("1") self.assertEqual(1, version.Major) self.assertEqual(0, version.Minor) def test_String_MajorMinor(self) -> None: version = CalendarVersion.Parse("1.2") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) @mark.xfail(reason="v2024.04 not yet support") def test_vString(self) -> None: version = CalendarVersion.Parse("v1.2") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) @mark.xfail(reason="i2024.04 not yet support") def test_iString(self) -> None: version = CalendarVersion.Parse("i1.2") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) @mark.xfail(reason="r2024.04 not yet support") def test_rString(self) -> None: version = CalendarVersion.Parse("r1.2") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) # class CompareVersions(TestCase): # def test_Equal(self) -> None: # l = [ # ("0.0.0", "0.0.0"), # ("0.0.1", "0.0.1"), # ("0.1.0", "0.1.0"), # ("1.0.0", "1.0.0"), # ("1.0.1", "1.0.1"), # ("1.1.0", "1.1.0"), # ("1.1.1", "1.1.1") # ] # # for t in l: # with self.subTest(equal=t): # v1 = CalendarVersion.Parse(t[0]) # v2 = CalendarVersion.Parse(t[1]) # self.assertEqual(v1, v2) # # def test_Unequal(self) -> None: # l = [ # ("0.0.0", "0.0.1"), # ("0.0.1", "0.0.0"), # ("0.0.0", "0.1.0"), # ("0.1.0", "0.0.0"), # ("0.0.0", "1.0.0"), # ("1.0.0", "0.0.0"), # ("1.0.1", "1.1.0"), # ("1.1.0", "1.0.1") # ] # # for t in l: # with self.subTest(unequal=t): # v1 = CalendarVersion.Parse(t[0]) # v2 = CalendarVersion.Parse(t[1]) # self.assertNotEqual(v1, v2) # # def test_LessThan(self) -> None: # l = [ # ("0.0.0", "0.0.1"), # ("0.0.0", "0.1.0"), # ("0.0.0", "1.0.0"), # ("0.0.1", "0.1.0"), # ("0.1.0", "1.0.0") # ] # # for t in l: # with self.subTest(lessthan=t): # v1 = CalendarVersion.Parse(t[0]) # v2 = CalendarVersion.Parse(t[1]) # self.assertLess(v1, v2) # # def test_LessEqual(self) -> None: # l = [ # ("0.0.0", "0.0.0"), # ("0.0.0", "0.0.1"), # ("0.0.0", "0.1.0"), # ("0.0.0", "1.0.0"), # ("0.0.1", "0.1.0"), # ("0.1.0", "1.0.0") # ] # # for t in l: # with self.subTest(lessequal=t): # v1 = CalendarVersion.Parse(t[0]) # v2 = CalendarVersion.Parse(t[1]) # self.assertLessEqual(v1, v2) # # def test_GreaterThan(self) -> None: # l = [ # ("0.0.1", "0.0.0"), # ("0.1.0", "0.0.0"), # ("1.0.0", "0.0.0"), # ("0.1.0", "0.0.1"), # ("1.0.0", "0.1.0") # ] # # for t in l: # with self.subTest(greaterthan=t): # v1 = CalendarVersion.Parse(t[0]) # v2 = CalendarVersion.Parse(t[1]) # self.assertGreater(v1, v2) # # def test_GreaterEqual(self) -> None: # l = [ # ("0.0.0", "0.0.0"), # ("0.0.1", "0.0.0"), # ("0.1.0", "0.0.0"), # ("1.0.0", "0.0.0"), # ("0.1.0", "0.0.1"), # ("1.0.0", "0.1.0") # ] # # for t in l: # with self.subTest(greaterequal=t): # v1 = CalendarVersion.Parse(t[0]) # v2 = CalendarVersion.Parse(t[1]) # self.assertGreaterEqual(v1, v2) class HashVersions(TestCase): def test_CalendarVersion(self) -> None: version = CalendarVersion.Parse("2024.2") self.assertIsNotNone(version.__hash__()) def test_YearMonthVersion(self) -> None: version = YearMonthVersion(2024, 2) self.assertIsNotNone(version.__hash__()) def test_YearWeekVersion(self) -> None: version = YearWeekVersion(2024, 42) self.assertIsNotNone(version.__hash__()) def test_YearReleaseVersion(self) -> None: version = YearReleaseVersion(2024, 25) self.assertIsNotNone(version.__hash__()) def test_YearMonthDayVersion(self) -> None: version = YearMonthDayVersion(2024, 8, 25) self.assertIsNotNone(version.__hash__()) class CompareNone(TestCase): def test_Equal(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(ValueError): _ = version == None def test_Unequal(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(ValueError): _ = version != None def test_LessThan(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(ValueError): _ = version < None def test_LessThanOrEqual(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(ValueError): _ = version <= None def test_GreaterThan(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(ValueError): _ = version > None def test_GreaterThanOrEqual(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(ValueError): _ = version >= None class CompareString(TestCase): def test_Equal(self) -> None: version = CalendarVersion(1, 2) self.assertEqual("1.2", version) def test_Unequal(self) -> None: version = CalendarVersion(1, 2) self.assertNotEqual("1.3", version) def test_LessThan(self) -> None: version = CalendarVersion(1, 2) self.assertLess("1.1", version) def test_LessThanOrEqual(self) -> None: version = CalendarVersion(1, 2) self.assertLessEqual("1.2", version) def test_GreaterThan(self) -> None: version = CalendarVersion(1, 2) self.assertGreater("1.3", version) def test_GreaterThanOrEqual(self) -> None: version = CalendarVersion(1, 2) self.assertGreaterEqual("1.2", version) class CompareInteger(TestCase): def test_Equal(self) -> None: version = CalendarVersion(1) self.assertEqual(1, version) def test_Unequal(self) -> None: version = CalendarVersion(1) self.assertNotEqual(2, version) def test_LessThan(self) -> None: version = CalendarVersion(1, 2) self.assertLess(0, version) def test_LessThanOrEqual(self) -> None: version = CalendarVersion(1, 2) self.assertLessEqual(1, version) def test_GreaterThan(self) -> None: version = CalendarVersion(1, 2) self.assertGreater(3, version) def test_GreaterThanOrEqual(self) -> None: version = CalendarVersion(1, 2) self.assertGreaterEqual(2, version) class CompareOtherType(TestCase): def test_Equal(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(TypeError): _ = version == 1.2 def test_Unequal(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(TypeError): _ = version != 1.2 def test_LessThan(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(TypeError): _ = version < 1.2 def test_LessThanOrEqual(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(TypeError): _ = version <= 1.2 def test_GreaterThan(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(TypeError): _ = version > 1.2 def test_GreaterThanOrEqual(self) -> None: version = CalendarVersion(1, 2) with self.assertRaises(TypeError): _ = version >= 1.2 class ValidatedWordSize(TestCase): def test_All8Bit_AllInRange(self) -> None: version = CalendarVersion.Parse("12.64", WordSizeValidator(8)) self.assertEqual(12, version.Major) self.assertEqual(64, version.Minor) def test_All8Bit_MajorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = CalendarVersion.Parse("1203.64", WordSizeValidator(8)) self.assertIn("Version.Major", str(ex.exception)) def test_All8Bit_MinorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = CalendarVersion.Parse("12.640", WordSizeValidator(8)) self.assertIn("Version.Minor", str(ex.exception)) def test_35Bits_AllInRange(self) -> None: version = CalendarVersion.Parse("7.31", WordSizeValidator(2, majorBits=3, minorBits=5)) self.assertEqual(7, version.Major) self.assertEqual(31, version.Minor) def test_35Bit_MajorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = CalendarVersion.Parse("8.31", WordSizeValidator(majorBits=3, minorBits=5)) self.assertIn("Version.Major", str(ex.exception)) def test_35Bit_MinorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = CalendarVersion.Parse("7.32", WordSizeValidator(8, majorBits=3, minorBits=5)) self.assertIn("Version.Minor", str(ex.exception)) class ValidatedMaxValue(TestCase): def test_All63_AllInRange(self) -> None: version = CalendarVersion.Parse("12.63", MaxValueValidator(63)) self.assertEqual(12, version.Major) self.assertEqual(63, version.Minor) def test_All63_MajorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = CalendarVersion.Parse("64.12", MaxValueValidator(63)) self.assertIn("Version.Major", str(ex.exception)) def test_All63_MinorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = CalendarVersion.Parse("12.64", MaxValueValidator(63)) self.assertIn("Version.Minor", str(ex.exception)) class FormattingUsingRepr(TestCase): def test_Major(self) -> None: version = CalendarVersion(1) self.assertEqual("1.0", repr(version)) @mark.xfail(reason="v2024.04 not yet support") def test_MajorPrefix(self) -> None: version = CalendarVersion(1, prefix="v") self.assertEqual("v1.0", repr(version)) def test_MajorMinor(self) -> None: version = CalendarVersion(1, 2) self.assertEqual("1.2", repr(version)) class FormattingUsingStr(TestCase): def test_Major(self) -> None: version = CalendarVersion(1) self.assertEqual("1", str(version)) @mark.xfail(reason="v2024.04 not yet support") def test_MajorPrefix(self) -> None: version = CalendarVersion(1, prefix="v") self.assertEqual("v1", str(version)) def test_MajorMinor(self) -> None: version = CalendarVersion(1, 2) self.assertEqual("1.2", str(version)) class FormattingUsingFormat(TestCase): def test_Empty(self) -> None: version = CalendarVersion(1, 2) self.assertEqual("1.2", f"{version:}") self.assertEqual(str(version), f"{version:}") def test_OtherFormat(self) -> None: version = CalendarVersion(1, 2, 3) self.assertEqual("hello world", f"{version:hello world}") def test_Percent(self) -> None: version = CalendarVersion(1, 2) self.assertEqual("hello%world", f"{version:hello%%world}") def test_Major(self) -> None: version = CalendarVersion(1, 2) self.assertEqual("1", f"{version:%M}") def test_Minor(self) -> None: version = CalendarVersion(1, 2) self.assertEqual("2", f"{version:%m}") def test_FullVersion(self) -> None: version = CalendarVersion(1, 2) self.assertEqual("v1.2", f"{version:v%M.%m}") class InstantiationOfYearMonthVersion(TestCase): def test_Year(self) -> None: version = YearMonthVersion(1) self.assertEqual(1, version.Year) self.assertEqual(0, version.Month) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) def test_YearMonth(self) -> None: version = YearMonthVersion(1, 2) self.assertEqual(1, version.Year) self.assertEqual(2, version.Month) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) class InstantiationOfYearWeekVersion(TestCase): def test_Year(self) -> None: version = YearWeekVersion(1) self.assertEqual(1, version.Year) self.assertEqual(0, version.Week) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) def test_YearWeek(self) -> None: version = YearWeekVersion(1, 2) self.assertEqual(1, version.Year) self.assertEqual(2, version.Week) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) class InstantiationOfYearReleaseVersion(TestCase): def test_Year(self) -> None: version = YearReleaseVersion(1) self.assertEqual(1, version.Year) self.assertEqual(0, version.Release) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) def test_YearRelease(self) -> None: version = YearReleaseVersion(1, 2) self.assertEqual(1, version.Year) self.assertEqual(2, version.Release) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) class InstantiationOfYearMonthDayVersion(TestCase): def test_Year(self) -> None: version = YearMonthDayVersion(1) self.assertEqual(1, version.Year) self.assertEqual(0, version.Month) self.assertEqual(0, version.Day) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) def test_YearMonth(self) -> None: version = YearMonthDayVersion(1, 2) self.assertEqual(1, version.Year) self.assertEqual(2, version.Month) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) def test_YearMonthDay(self) -> None: version = YearMonthDayVersion(1, 2, 3) self.assertEqual(1, version.Year) self.assertEqual(2, version.Month) self.assertEqual(3, version.Day) self.assertEqual(0, version.Build) self.assertEqual(Flags.Clean, version.Flags) pyTooling-8.11.0/tests/unit/Versioning/Range.py000066400000000000000000000246541513317154500214570ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for package :mod:`pyTooling.Versioning`.""" from unittest import TestCase from pyTooling.Versioning import SemanticVersion, PythonVersion, CalendarVersion, VersionRange, RangeBoundHandling if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_SemVer_SemVer(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2) self.assertIs(v1, vr.LowerBound) self.assertIs(v2, vr.UpperBound) self.assertEqual(RangeBoundHandling.BothBoundsInclusive, vr.BoundHandling) def test_SemVer_SemVer_Reverse(self) -> None: v1 = SemanticVersion(2, 0, 0) v2 = SemanticVersion(1, 0, 0) with self.assertRaises(ValueError) as ex: _ = VersionRange(v1, v2) def test_SemVer_Tuple(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = (2, 0, 0) with self.assertRaises(TypeError) as ex: _ = VersionRange(v1, v2) def test_Tuple_SemVer(self) -> None: v1 = (1, 0, 0) v2 = SemanticVersion(2, 0, 0) with self.assertRaises(TypeError) as ex: _ = VersionRange(v1, v2) def test_SemVer_CalVer(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = CalendarVersion(2, 0, 0) with self.assertRaises(TypeError) as ex: _ = VersionRange(v1, v2) def test_CalVer_SemVer(self) -> None: v1 = CalendarVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) with self.assertRaises(TypeError) as ex: _ = VersionRange(v1, v2) def test_SemVer_PyVer(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = PythonVersion(2, 0, 0) vr = VersionRange(v1, v2, RangeBoundHandling.LowerBoundExclusive) self.assertIs(v1, vr.LowerBound) self.assertIs(v2, vr.UpperBound) self.assertEqual(RangeBoundHandling.LowerBoundExclusive, vr.BoundHandling) def test_PyVer_SemVer(self) -> None: v1 = PythonVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2, RangeBoundHandling.UpperBoundExclusive) self.assertIs(v1, vr.LowerBound) self.assertIs(v2, vr.UpperBound) self.assertEqual(RangeBoundHandling.UpperBoundExclusive, vr.BoundHandling) class Comparison(TestCase): def test_LessThan(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2) self.assertTrue(SemanticVersion(0, 5, 0) < vr) self.assertFalse(SemanticVersion(1, 5, 0) < vr) self.assertFalse(SemanticVersion(2, 5, 0) < vr) self.assertFalse(vr < SemanticVersion(0, 5, 0)) self.assertFalse(vr < SemanticVersion(1, 5, 0)) self.assertTrue(vr < SemanticVersion(2, 5, 0)) def test_LessThan_WrongType(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2) with self.assertRaises(TypeError) as ex: _ = vr < (2, 5, 0) def test_LessThanOrEqual(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2) self.assertTrue(SemanticVersion(0, 5, 0) <= vr) self.assertTrue(SemanticVersion(1, 0, 0) <= vr) self.assertFalse(SemanticVersion(1, 5, 0) <= vr) self.assertFalse(SemanticVersion(2, 0, 0) <= vr) self.assertFalse(SemanticVersion(2, 5, 0) <= vr) self.assertFalse(vr <= SemanticVersion(0, 5, 0)) self.assertFalse(vr <= SemanticVersion(1, 0, 0)) self.assertFalse(vr <= SemanticVersion(1, 5, 0)) self.assertTrue(vr <= SemanticVersion(2, 0, 0)) self.assertTrue(vr <= SemanticVersion(2, 5, 0)) def test_LessThanOrEqual_Exclusive(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2, RangeBoundHandling.BothBoundsExclusive) self.assertTrue(SemanticVersion(0, 5, 0) <= vr) self.assertFalse(SemanticVersion(1, 0, 0) <= vr) self.assertFalse(SemanticVersion(1, 5, 0) <= vr) self.assertFalse(SemanticVersion(2, 0, 0) <= vr) self.assertFalse(SemanticVersion(2, 5, 0) <= vr) self.assertFalse(vr <= SemanticVersion(0, 5, 0)) self.assertFalse(vr <= SemanticVersion(1, 0, 0)) self.assertFalse(vr <= SemanticVersion(1, 5, 0)) self.assertFalse(vr <= SemanticVersion(2, 0, 0)) self.assertTrue(vr <= SemanticVersion(2, 5, 0)) def test_GreaterThan(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2) self.assertFalse(SemanticVersion(0, 5, 0) > vr) self.assertFalse(SemanticVersion(1, 5, 0) > vr) self.assertTrue(SemanticVersion(2, 5, 0) > vr) self.assertTrue(vr > SemanticVersion(0, 5, 0)) self.assertFalse(vr > SemanticVersion(1, 5, 0)) self.assertFalse(vr > SemanticVersion(2, 5, 0)) def test_GreaterThanOrEqual(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2) self.assertFalse(SemanticVersion(0, 5, 0) >= vr) self.assertFalse(SemanticVersion(1, 0, 0) >= vr) self.assertFalse(SemanticVersion(1, 5, 0) >= vr) self.assertTrue(SemanticVersion(2, 0, 0) >= vr) self.assertTrue(SemanticVersion(2, 5, 0) >= vr) self.assertTrue(vr >= SemanticVersion(0, 5, 0)) self.assertTrue(vr >= SemanticVersion(1, 0, 0)) self.assertFalse(vr >= SemanticVersion(1, 5, 0)) self.assertFalse(vr >= SemanticVersion(2, 0, 0)) self.assertFalse(vr >= SemanticVersion(2, 5, 0)) def test_In(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vr = VersionRange(v1, v2) self.assertTrue(SemanticVersion(0, 5, 0) not in vr) self.assertTrue(SemanticVersion(1, 0, 0) in vr) self.assertTrue(SemanticVersion(1, 5, 0) in vr) self.assertTrue(SemanticVersion(2, 0, 0) in vr) self.assertTrue(SemanticVersion(2, 5, 0) not in vr) class Intersection(TestCase): def test_AInsideB(self) -> None: vA1 = SemanticVersion(2, 0, 0) vA2 = SemanticVersion(3, 0, 0) vrA = VersionRange(vA1, vA2) vB1 = SemanticVersion(1, 0, 0) vB2 = SemanticVersion(4, 0, 0) vrB = VersionRange(vB1, vB2) intersection = vrA & vrB self.assertEqual(vA1, intersection.LowerBound) self.assertEqual(vA2, intersection.UpperBound) def test_BInsideA(self) -> None: vA1 = SemanticVersion(1, 0, 0) vA2 = SemanticVersion(4, 0, 0) vrA = VersionRange(vA1, vA2) vB1 = SemanticVersion(2, 0, 0) vB2 = SemanticVersion(3, 0, 0) vrB = VersionRange(vB1, vB2) intersection = vrA & vrB self.assertEqual(vB1, intersection.LowerBound) self.assertEqual(vB2, intersection.UpperBound) def test_ALeftInnerB(self) -> None: vA1 = SemanticVersion(1, 0, 0) vA2 = SemanticVersion(3, 0, 0) vrA = VersionRange(vA1, vA2) vB1 = SemanticVersion(2, 0, 0) vB2 = SemanticVersion(4, 0, 0) vrB = VersionRange(vB1, vB2) intersection = vrA & vrB self.assertEqual(vB1, intersection.LowerBound) self.assertEqual(vA2, intersection.UpperBound) def test_ARightInnerB(self) -> None: vA1 = SemanticVersion(3, 0, 0) vA2 = SemanticVersion(5, 0, 0) vrA = VersionRange(vA1, vA2) vB1 = SemanticVersion(2, 0, 0) vB2 = SemanticVersion(4, 0, 0) vrB = VersionRange(vB1, vB2) intersection = vrA & vrB self.assertEqual(vA1, intersection.LowerBound) self.assertEqual(vB2, intersection.UpperBound) pyTooling-8.11.0/tests/unit/Versioning/SemVersion.py000066400000000000000000000755341513317154500225200ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2020-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for package :mod:`pyTooling.Versioning`.""" from unittest import TestCase from pyTooling.Versioning import Flags, ReleaseLevel, SemanticVersion, WordSizeValidator, MaxValueValidator if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_Major(self) -> None: version = SemanticVersion(1) self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(0, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(0, version.Patch) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) self.assertEqual(0, version.ReleaseNumber) self.assertEqual(0, version.Post) self.assertEqual(0, version.Dev) self.assertEqual(0, version.Build) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinor(self) -> None: version = SemanticVersion(1, 2) self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(0, version.Patch) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) self.assertEqual(0, version.ReleaseNumber) self.assertEqual(0, version.Post) self.assertEqual(0, version.Dev) self.assertEqual(0, version.Build) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicro(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) self.assertEqual(0, version.ReleaseNumber) self.assertEqual(0, version.Post) self.assertEqual(0, version.Dev) self.assertEqual(0, version.Build) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevel(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha) self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(0, version.ReleaseNumber) self.assertEqual(0, version.Post) self.assertEqual(0, version.Dev) self.assertEqual(0, version.Build) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevelNumber(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4) self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(4, version.ReleaseNumber) self.assertEqual(0, version.Post) self.assertEqual(0, version.Dev) self.assertEqual(0, version.Build) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevelNumberPost(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5) self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(4, version.ReleaseNumber) self.assertEqual(5, version.Post) self.assertEqual(0, version.Dev) self.assertEqual(0, version.Build) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevelNumberPostDev(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6) self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(4, version.ReleaseNumber) self.assertEqual(5, version.Post) self.assertEqual(6, version.Dev) self.assertEqual(0, version.Build) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevelNumberPostDevBuild(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7) self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(4, version.ReleaseNumber) self.assertEqual(5, version.Post) self.assertEqual(6, version.Dev) self.assertEqual(7, version.Build) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevelNumberPostDevBuildPostfix(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix="p") self.assertEqual("", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(4, version.ReleaseNumber) self.assertEqual(5, version.Post) self.assertEqual(6, version.Dev) self.assertEqual(7, version.Build) self.assertEqual("p", version.Postfix) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevelNumberPostDevBuildPostfixPrefix(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix="p", prefix="v") self.assertEqual("v", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(4, version.ReleaseNumber) self.assertEqual(5, version.Post) self.assertEqual(6, version.Dev) self.assertEqual(7, version.Build) self.assertEqual("p", version.Postfix) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevelNumberPostDevBuildPostfixPrefixHash(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix="p", prefix="v", hash="abcdef") self.assertEqual("v", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(4, version.ReleaseNumber) self.assertEqual(5, version.Post) self.assertEqual(6, version.Dev) self.assertEqual(7, version.Build) self.assertEqual("p", version.Postfix) self.assertEqual(Flags.NoVCS, version.Flags) def test_MajorMinorMicroReleaseLevelNumberPostDevBuildPostfixPrefixHashFlags(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix="p", prefix="v", hash="abcdef", flags=Flags.Git) self.assertEqual("v", version.Prefix) self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(3, version.Patch) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(4, version.ReleaseNumber) self.assertEqual(5, version.Post) self.assertEqual(6, version.Dev) self.assertEqual(7, version.Build) self.assertEqual("p", version.Postfix) self.assertEqual("abcdef", version.Hash) self.assertEqual(Flags.Git, version.Flags) def test_Major_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion("1") def test_Major_Negative(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(-1) def test_Major_Minor_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, "2") def test_Major_Minor_Negative(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, -2) def test_Major_Micro_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, "3") def test_Major_Micro_Negative(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, 2,-3) def test_Major_ReleaseLevel_None(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, 2, 3, None) def test_Major_ReleaseLevel_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, "RL") def test_Major_ReleaseLevel_Final(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Final, 1) def test_Major_Number_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, "4") def test_Major_Number_Negative(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, -4) def test_Major_Postv_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, "5") def test_Major_Post_Negative(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, -5) def test_Major_Dev_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, "6") def test_Major_Dev_Negative(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, -6) def test_Major_Build_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build="7") def test_Major_Build_Negative(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=-7) def test_Major_Postfix_Integer(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix=8) def test_Major_Prefix_Integer(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix="p", prefix=9) def test_Major_Hash_Integer(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix="p", prefix="v", hash=10) def test_Major_Flags_None(self) -> None: with self.assertRaises(ValueError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix="p", prefix="v", hash="ab", flags=None) def test_Major_Flags_String(self) -> None: with self.assertRaises(TypeError): _ = SemanticVersion(1, 2, 3, ReleaseLevel.Alpha, 4, 5, 6, build=7, postfix="p", prefix="v", hash="ab", flags="d") class Parsing(TestCase): def test_None(self) -> None: with self.assertRaises(ValueError): SemanticVersion.Parse(None) def test_EmptyString(self) -> None: with self.assertRaises(ValueError): SemanticVersion.Parse("") def test_OtherType(self) -> None: with self.assertRaises(TypeError): SemanticVersion.Parse(1) def test_InvalidString(self) -> None: with self.assertRaises(ValueError): SemanticVersion.Parse("None") def test_String_Major(self) -> None: version = SemanticVersion.Parse("1") self.assertEqual(1, version.Major) self.assertEqual(0, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) def test_String_MajorMinor(self) -> None: version = SemanticVersion.Parse("1.2") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) def test_String_MajorMinorMicro(self) -> None: version = SemanticVersion.Parse("1.2.3") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) def test_vString(self) -> None: version = SemanticVersion.Parse("v1.2.3") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) def test_iString(self) -> None: version = SemanticVersion.Parse("i1.2.3") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) def test_rString(self) -> None: version = SemanticVersion.Parse("r1.2.3") self.assertEqual(1, version.Major) self.assertEqual(2, version.Minor) self.assertEqual(3, version.Micro) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) def test_MajorMinorDev(self) -> None: version = SemanticVersion.Parse("0.6-dev") self.assertEqual(0, version.Major) self.assertEqual(6, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(ReleaseLevel.Development, version.ReleaseLevel) self.assertEqual(0, version.ReleaseNumber) def test_MajorMinorDevelopment(self) -> None: version = SemanticVersion.Parse("0.6.dev10") self.assertEqual(0, version.Major) self.assertEqual(6, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(ReleaseLevel.Final, version.ReleaseLevel) self.assertEqual(10, version.Dev) def test_MajorMinorAlpha(self) -> None: version = SemanticVersion.Parse("0.6a1") self.assertEqual(0, version.Major) self.assertEqual(6, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(ReleaseLevel.Alpha, version.ReleaseLevel) self.assertEqual(1, version.ReleaseNumber) def test_MajorMinorBeta(self) -> None: version = SemanticVersion.Parse("0.6b5") self.assertEqual(0, version.Major) self.assertEqual(6, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(ReleaseLevel.Beta, version.ReleaseLevel) self.assertEqual(5, version.ReleaseNumber) def test_MajorMinorGamma(self) -> None: version = SemanticVersion.Parse("0.6c3") self.assertEqual(0, version.Major) self.assertEqual(6, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(ReleaseLevel.Gamma, version.ReleaseLevel) self.assertEqual(3, version.ReleaseNumber) def test_MajorMinorReleaseCandidate(self) -> None: version = SemanticVersion.Parse("0.6rc2") self.assertEqual(0, version.Major) self.assertEqual(6, version.Minor) self.assertEqual(0, version.Micro) self.assertEqual(ReleaseLevel.ReleaseCandidate, version.ReleaseLevel) self.assertEqual(2, version.ReleaseNumber) class HashVersions(TestCase): def test_SemanticVersion(self) -> None: version = SemanticVersion.Parse("v1.2.3") self.assertIsNotNone(version.__hash__()) class CompareVersions(TestCase): def test_Equal(self) -> None: l = [ ("0.0.0", "0.0.0"), ("0.0.1", "0.0.1"), ("0.1.0", "0.1.0"), ("1.0.0", "1.0.0"), ("1.0.1", "1.0.1"), ("1.1.0", "1.1.0"), ("1.1.1", "1.1.1") ] for t in l: with self.subTest(equal=t): v1 = SemanticVersion.Parse(t[0]) v2 = SemanticVersion.Parse(t[1]) self.assertEqual(v1, v2) def test_Unequal(self) -> None: l = [ ("0.0.0", "0.0.1"), ("0.0.1", "0.0.0"), ("0.0.0", "0.1.0"), ("0.1.0", "0.0.0"), ("0.0.0", "1.0.0"), ("1.0.0", "0.0.0"), ("1.0.1", "1.1.0"), ("1.1.0", "1.0.1") ] for t in l: with self.subTest(unequal=t): v1 = SemanticVersion.Parse(t[0]) v2 = SemanticVersion.Parse(t[1]) self.assertNotEqual(v1, v2) def test_LessThan(self) -> None: l = [ ("0.0.0", "0.0.1"), ("0.0.0", "0.1.0"), ("0.0.0", "1.0.0"), ("0.0.1", "0.1.0"), ("0.1.0", "1.0.0") ] for t in l: with self.subTest(lessthan=t): v1 = SemanticVersion.Parse(t[0]) v2 = SemanticVersion.Parse(t[1]) self.assertLess(v1, v2) def test_LessEqual(self) -> None: l = [ ("0.0.0", "0.0.0"), ("0.0.0", "0.0.1"), ("0.0.0", "0.1.0"), ("0.0.0", "1.0.0"), ("0.0.1", "0.1.0"), ("0.1.0", "1.0.0") ] for t in l: with self.subTest(lessequal=t): v1 = SemanticVersion.Parse(t[0]) v2 = SemanticVersion.Parse(t[1]) self.assertLessEqual(v1, v2) def test_GreaterThan(self) -> None: l = [ ("0.0.1", "0.0.0"), ("0.1.0", "0.0.0"), ("1.0.0", "0.0.0"), ("0.1.0", "0.0.1"), ("1.0.0", "0.1.0") ] for t in l: with self.subTest(greaterthan=t): v1 = SemanticVersion.Parse(t[0]) v2 = SemanticVersion.Parse(t[1]) self.assertGreater(v1, v2) def test_GreaterEqual(self) -> None: l = [ ("0.0.0", "0.0.0"), ("0.0.1", "0.0.0"), ("0.1.0", "0.0.0"), ("1.0.0", "0.0.0"), ("0.1.0", "0.0.1"), ("1.0.0", "0.1.0") ] for t in l: with self.subTest(greaterequal=t): v1 = SemanticVersion.Parse(t[0]) v2 = SemanticVersion.Parse(t[1]) self.assertGreaterEqual(v1, v2) def test_Minimum(self) -> None: l = [ # ver req exp ("0.0.1", "0.0.0", True), ("0.0.1", "0.0.1", True), ("0.0.1", "0.0.2", False), ("0.1.0", "0.0", True), ("0.1.0", "0.1", True), ("0.1.0", "0.2", False), ("1.0.0", "0", True), ("1.0.0", "1", True), ("1.0.0", "2", False), ] for ver, req, exp in l: with self.subTest(minimum=(ver, req)): version = SemanticVersion.Parse(ver) requirement = SemanticVersion.Parse(req) self.assertEqual(exp, version >> requirement, f"{version} ~= {requirement}") class CompareNone(TestCase): def test_Equal(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(ValueError): _ = version == None def test_Unequal(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(ValueError): _ = version != None def test_LessThan(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(ValueError): _ = version < None def test_LessThanOrEqual(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(ValueError): _ = version <= None def test_GreaterThan(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(ValueError): _ = version > None def test_GreaterThanOrEqual(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(ValueError): _ = version >= None def test_Minimum(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(ValueError): _ = version >> None class CompareString(TestCase): def test_Equal(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("1.2.3", version) def test_Unequal(self) -> None: version = SemanticVersion(1, 2, 3) self.assertNotEqual("1.2.4", version) def test_LessThan(self) -> None: version = SemanticVersion(1, 2, 3) self.assertLess("1.2.2", version) def test_LessThanOrEqual(self) -> None: version = SemanticVersion(1, 2, 3) self.assertLessEqual("1.2.3", version) def test_GreaterThan(self) -> None: version = SemanticVersion(1, 2, 3) self.assertGreater("1.2.4", version) def test_GreaterThanOrEqual(self) -> None: version = SemanticVersion(1, 2, 3) self.assertGreaterEqual("1.2.3", version) def test_Minimum(self) -> None: version = SemanticVersion(1, 2, 3) self.assertTrue(version >> "1.2.3") class CompareInteger(TestCase): def test_Equal(self) -> None: version = SemanticVersion(1) self.assertEqual(1, version) def test_Unequal(self) -> None: version = SemanticVersion(1) self.assertNotEqual(2, version) def test_LessThan(self) -> None: version = SemanticVersion(1, 2, 3) self.assertLess(0, version) def test_LessThanOrEqual(self) -> None: version = SemanticVersion(1, 2, 3) self.assertLessEqual(1, version) def test_GreaterThan(self) -> None: version = SemanticVersion(1, 2, 3) self.assertGreater(3, version) def test_GreaterThanOrEqual(self) -> None: version = SemanticVersion(1, 2, 3) self.assertGreaterEqual(2, version) def test_Minimum(self) -> None: version = SemanticVersion(1, 2, 3) self.assertTrue(version >> 1) class CompareOtherType(TestCase): def test_Equal(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(TypeError): _ = version == 1.2 def test_Unequal(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(TypeError): _ = version != 1.2 def test_LessThan(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(TypeError): _ = version < 1.2 def test_LessThanOrEqual(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(TypeError): _ = version <= 1.2 def test_GreaterThan(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(TypeError): _ = version > 1.2 def test_GreaterThanOrEqual(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(TypeError): _ = version >= 1.2 def test_Minimum(self) -> None: version = SemanticVersion(1, 2, 3) with self.assertRaises(TypeError): _ = version >> 1.2 class ValidatedWordSize(TestCase): def test_All8Bit_AllInRange(self) -> None: version = SemanticVersion.Parse("12.64.255", WordSizeValidator(8)) self.assertEqual(12, version.Major) self.assertEqual(64, version.Minor) self.assertEqual(255, version.Micro) def test_All8Bit_MajorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("1203.64.255", WordSizeValidator(8)) self.assertIn("Version.Major", str(ex.exception)) def test_All8Bit_MinorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("12.640.255", WordSizeValidator(8)) self.assertIn("Version.Minor", str(ex.exception)) def test_All8Bit_MicroOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("12.64.256", WordSizeValidator(8)) self.assertIn("Version.Micro", str(ex.exception)) def test_358Bits_AllInRange(self) -> None: version = SemanticVersion.Parse("7.31.255", WordSizeValidator(2, majorBits=3, minorBits=5, microBits=8)) self.assertEqual(7, version.Major) self.assertEqual(31, version.Minor) self.assertEqual(255, version.Micro) def test_358Bit_MajorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("8.31.255", WordSizeValidator(majorBits=3, minorBits=5, microBits=8)) self.assertIn("Version.Major", str(ex.exception)) def test_358Bit_MinorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("7.32.255", WordSizeValidator(8, majorBits=3, minorBits=5)) self.assertIn("Version.Minor", str(ex.exception)) def test_358Bit_MicroOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("7.31.256", WordSizeValidator(8, majorBits=3, minorBits=5)) self.assertIn("Version.Micro", str(ex.exception)) class ValidatedMaxValue(TestCase): def test_All255_AllInRange(self) -> None: version = SemanticVersion.Parse("12.64.255", MaxValueValidator(255)) self.assertEqual(12, version.Major) self.assertEqual(64, version.Minor) self.assertEqual(255, version.Micro) def test_All255_MajorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("1203.64.255", MaxValueValidator(255)) self.assertIn("Version.Major", str(ex.exception)) def test_All255_MinorOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("12.640.255", MaxValueValidator(255)) self.assertIn("Version.Minor", str(ex.exception)) def test_All255_MicroOutOfRange(self) -> None: with self.assertRaises(ValueError) as ex: _ = SemanticVersion.Parse("12.64.256", MaxValueValidator(255)) self.assertIn("Version.Micro", str(ex.exception)) class FormattingUsingRepr(TestCase): def test_Major(self) -> None: version = SemanticVersion(1) self.assertEqual("1.0.0", repr(version)) def test_MajorPrefix(self) -> None: version = SemanticVersion(1) self.assertEqual("1.0.0", repr(version)) def test_MajorMinor(self) -> None: version = SemanticVersion(1, 2) self.assertEqual("1.2.0", repr(version)) def test_MajorMinorMicro(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("1.2.3", repr(version)) class FormattingUsingStr(TestCase): def test_Major(self) -> None: version = SemanticVersion(1) self.assertEqual("1", str(version)) def test_MajorPrefix(self) -> None: version = SemanticVersion(1, prefix="v") self.assertEqual("v1", str(version)) def test_MajorMinor(self) -> None: version = SemanticVersion(1, 2) self.assertEqual("1.2", str(version)) def test_MajorMinorMicro(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("1.2.3", str(version)) def test_MajorMinorMicroPrefix(self) -> None: version = SemanticVersion(1, 2, 3, prefix="v") self.assertEqual("v1.2.3", str(version)) class FormattingUsingFormat(TestCase): def test_Empty(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("1.2.3", f"{version:}") self.assertEqual(str(version), f"{version:}") def test_OtherFormat(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("hello world", f"{version:hello world}") def test_Percent(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("hello%world", f"{version:hello%%world}") def test_Major(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("1", f"{version:%M}") def test_Major_Prefix(self) -> None: version = SemanticVersion(1, prefix="i") self.assertEqual("i", f"{version:%p}") def test_Minor(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("2", f"{version:%m}") def test_Micro(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("3", f"{version:%u}") def test_Build(self) -> None: version = SemanticVersion(1, 2, 3) self.assertEqual("0", f"{version:%b}") def test_ReleaseLevel_Short(self) -> None: version = SemanticVersion(1, 2, 3, level=ReleaseLevel.Alpha) self.assertEqual("a", f"{version:%r}") def test_ReleaseLevel_Short_Number(self) -> None: version = SemanticVersion(1, 2, 3, level=ReleaseLevel.Alpha) self.assertEqual("a0", f"{version:%r%n}") def test_ReleaseLevel_Long(self) -> None: version = SemanticVersion(1, 2, 3, level=ReleaseLevel.Alpha) self.assertEqual("alpha", f"{version:%R}") def test_ReleaseLevel_Long_Number(self) -> None: version = SemanticVersion(1, 2, 3, level=ReleaseLevel.Alpha, number=4) self.assertEqual("alpha-4", f"{version:%R-%n}") def test_FullVersion1(self) -> None: version = SemanticVersion(1, 2, 3, build=0, prefix="v") self.assertEqual("v1.2.3.0", f"{version:%p%M.%m.%u.%b}") def test_FullVersion2(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.ReleaseCandidate, 3, prefix="r") self.assertEqual("r1.2.3.rc3", f"{version:%p%M.%m.%u.%R%n}") def test_FullVersion3(self) -> None: version = SemanticVersion(1, 2, 3, ReleaseLevel.ReleaseCandidate, 3, prefix="v", postfix="deb25") self.assertEqual("v1.2.3-rc3+deb25", f"{version:%p%M.%m.%u-%R%n+%P}") class RoundTrip(TestCase): def test_Parse2Str(self) -> None: l = [ "1", "11.2", "11.12.3", "11.12.13.4", "v1", "v1.12", "v1.2.13", "v1.2.3.14", "r1.0", "i1.0", "i1.0+deb3", "rev1.2", "rev1.2+deb3", "v1.2.3-dev", "v1.2.3.dev23", "v1.2.3.alpha1", "v1.2.3.beta1", "v1.2.3.rc1", "v1.2.3.rc1+deb25", "1.2.rc3.post2", "1.2.rc3.post2.dev4", "v1.2.3.alpha4.post5.dev6+deb11u3" ] for ver in l: with self.subTest(version=ver): version = SemanticVersion.Parse(ver) self.assertEqual(ver, str(version), ver) def test_Parse2Str_Normalizing(self) -> None: ver = "01.02.03.04" version = SemanticVersion.Parse(ver) self.assertEqual(ver.replace("0", ""), str(version), ver) ver = "v01.02.03.rc04.post05.dev06+deb07" version = SemanticVersion.Parse(ver) self.assertEqual(ver.replace("0", "", 6), str(version), ver) pyTooling-8.11.0/tests/unit/Versioning/Set.py000066400000000000000000000252561513317154500211550ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for package :mod:`pyTooling.Versioning`.""" from unittest import TestCase from pytest import mark from pyTooling.Versioning import SemanticVersion, PythonVersion, CalendarVersion, VersionSet if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class Instantiation(TestCase): def test_None(self) -> None: with self.assertRaises(ValueError): _ = VersionSet(None) def test_SemVer(self) -> None: v = SemanticVersion(1, 0, 0) vs = VersionSet(v) self.assertEqual(v, vs[0]) def test_MultipleSemVer(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) vs = VersionSet((v2, v3, v1)) self.assertEqual(v1, vs[0]) self.assertEqual(v2, vs[1]) self.assertEqual(v3, vs[2]) def test_SemVer_CalVer(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = CalendarVersion(2, 0, 0) with self.assertRaises(TypeError): _ = VersionSet((v1, v2)) def test_SemVer_PyVer(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = PythonVersion(2, 0, 0) vs = VersionSet((v1, v2)) self.assertEqual(v1, vs[0]) self.assertEqual(v2, vs[1]) @mark.xfail(reason="An idea is needed how to check for compatible types in set.") def test_PyVer_SemVer(self) -> None: v1 = PythonVersion(1, 0, 0) v2 = SemanticVersion(2, 0, 0) vs = VersionSet((v1, v2)) self.assertEqual(v1, vs[0]) self.assertEqual(v2, vs[1]) class Comparison(TestCase): def test_LessThan(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) v4 = SemanticVersion(2, 5, 0) v5 = SemanticVersion(3, 0, 0) vs = VersionSet((v1, v2, v3, v4, v5)) self.assertTrue(SemanticVersion(0, 5, 0) < vs) self.assertFalse(SemanticVersion(1, 0, 0) < vs) self.assertFalse(SemanticVersion(1, 5, 0) < vs) self.assertFalse(vs < SemanticVersion(2, 5, 0)) self.assertFalse(vs < SemanticVersion(3, 0, 0)) self.assertTrue(vs < SemanticVersion(3, 5, 0)) def test_LessThan_WrongType(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) v4 = SemanticVersion(2, 5, 0) v5 = SemanticVersion(3, 0, 0) vs = VersionSet((v1, v2, v3, v4, v5)) with self.assertRaises(TypeError) as ex: _ = vs < (2, 5, 0) def test_LessThanOrEqual(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) v4 = SemanticVersion(2, 5, 0) v5 = SemanticVersion(3, 0, 0) vs = VersionSet((v1, v2, v3, v4, v5)) self.assertTrue(SemanticVersion(0, 5, 0) <= vs) self.assertTrue(SemanticVersion(1, 0, 0) <= vs) self.assertFalse(SemanticVersion(1, 5, 0) <= vs) self.assertFalse(vs <= SemanticVersion(2, 5, 0)) self.assertTrue(vs <= SemanticVersion(3, 0, 0)) self.assertTrue(vs <= SemanticVersion(3, 5, 0)) def test_GreaterThan(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) v4 = SemanticVersion(2, 5, 0) v5 = SemanticVersion(3, 0, 0) vs = VersionSet((v1, v2, v3, v4, v5)) self.assertFalse(SemanticVersion(2, 5, 0) > vs) self.assertFalse(SemanticVersion(3, 0, 0) > vs) self.assertTrue(SemanticVersion(3, 5, 0) > vs) self.assertTrue(vs > SemanticVersion(0, 5, 0)) self.assertFalse(vs > SemanticVersion(1, 0, 0)) self.assertFalse(vs > SemanticVersion(1, 5, 0)) def test_GreaterThanOrEqual(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) v4 = SemanticVersion(2, 5, 0) v5 = SemanticVersion(3, 0, 0) vs = VersionSet((v1, v2, v3, v4, v5)) self.assertFalse(SemanticVersion(2, 5, 0) >= vs) self.assertTrue(SemanticVersion(3, 0, 0) >= vs) self.assertTrue(SemanticVersion(3, 5, 0) >= vs) self.assertTrue(vs >= SemanticVersion(0, 5, 0)) self.assertTrue(vs >= SemanticVersion(1, 0, 0)) self.assertFalse(vs >= SemanticVersion(1, 5, 0)) def test_In(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) vF = SemanticVersion(2, 2, 0) vs = VersionSet((v2, v3, v1)) self.assertTrue(v2 in vs) self.assertFalse(vF in vs) class Ordering(TestCase): def test_Index(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) vs = VersionSet((v2, v3, v1)) previousVersion = vs[0] for i, nextVersion in enumerate(vs[1:]): self.assertLessEqual(previousVersion, vs[i]) previousVersion = vs[i] def test_Iterator(self) -> None: v1 = SemanticVersion(1, 0, 0) v2 = SemanticVersion(1, 5, 0) v3 = SemanticVersion(2, 0, 0) vs = VersionSet((v2, v3, v1)) iterator = iter(vs) previousVersion = next(iterator) for nextVersion in iterator: self.assertLessEqual(previousVersion, nextVersion) previousVersion = nextVersion class Intersection(TestCase): def test_EqualLists(self) -> None: vA1 = SemanticVersion(1, 0, 0) vA2 = SemanticVersion(1, 5, 0) vA3 = SemanticVersion(2, 0, 0) vsA = VersionSet((vA3, vA2, vA1)) vB1 = SemanticVersion(1, 0, 0) vB2 = SemanticVersion(1, 5, 0) vB3 = SemanticVersion(2, 0, 0) vsB = VersionSet((vB2, vB3, vB1)) intersection = vsA & vsB self.assertEqual(3, len(intersection)) self.assertEqual(vA1, intersection[0]) self.assertEqual(vA2, intersection[1]) self.assertEqual(vA3, intersection[2]) def test_EmptyResult(self) -> None: vA1 = SemanticVersion(1, 0, 0) vA2 = SemanticVersion(1, 5, 0) vA3 = SemanticVersion(2, 0, 0) vsA = VersionSet((vA3, vA2, vA1)) vB1 = SemanticVersion(1, 1, 0) vB2 = SemanticVersion(1, 4, 0) vB3 = SemanticVersion(2, 2, 0) vsB = VersionSet((vB2, vB3, vB1)) intersection = vsA & vsB self.assertEqual(0, len(intersection)) def test_Small_Big(self) -> None: vA1 = SemanticVersion(1, 0, 0) vA2 = SemanticVersion(1, 2, 0) vA3 = SemanticVersion(1, 3, 0) vA4 = SemanticVersion(1, 5, 0) vA5 = SemanticVersion(1, 8, 0) vsA = VersionSet((vA3, vA2, vA1, vA4, vA5)) vB1 = SemanticVersion(1, 2, 0) vB2 = SemanticVersion(1, 3, 0) vB3 = SemanticVersion(1, 3, 0) vB4 = SemanticVersion(1, 8, 0) vB5 = SemanticVersion(2, 0, 0) vsB = VersionSet((vB2, vB3, vB1, vB4, vB5)) intersection = vsA & vsB self.assertEqual(3, len(intersection)) self.assertEqual(vA2, intersection[0]) self.assertEqual(vA3, intersection[1]) self.assertEqual(vA5, intersection[2]) class Union(TestCase): def test_EqualLists(self) -> None: vA1 = SemanticVersion(1, 0, 0) vA2 = SemanticVersion(1, 5, 0) vA3 = SemanticVersion(2, 0, 0) vsA = VersionSet((vA3, vA2, vA1)) vB1 = SemanticVersion(1, 0, 0) vB2 = SemanticVersion(1, 5, 0) vB3 = SemanticVersion(2, 0, 0) vsB = VersionSet((vB2, vB3, vB1)) union = vsA | vsB self.assertEqual(3, len(union)) self.assertEqual(vA1, union[0]) self.assertEqual(vA2, union[1]) self.assertEqual(vA3, union[2]) def test_MergedLists(self) -> None: vA1 = SemanticVersion(1, 0, 0) vA2 = SemanticVersion(1, 5, 0) vA3 = SemanticVersion(2, 0, 0) vsA = VersionSet((vA3, vA2, vA1)) vB1 = SemanticVersion(1, 1, 0) vB2 = SemanticVersion(1, 4, 0) vB3 = SemanticVersion(2, 2, 0) vsB = VersionSet((vB2, vB3, vB1)) union = vsA | vsB self.assertEqual(6, len(union)) pyTooling-8.11.0/tests/unit/Warning/000077500000000000000000000000001513317154500173205ustar00rootroot00000000000000pyTooling-8.11.0/tests/unit/Warning/__init__.py000066400000000000000000000205161513317154500214350ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ __ __ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /_ _ _ __ _ __ (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ /\ / / _` | '__| '_ \| | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V V / (_| | | | | | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/\_/ \__,_|_| |_| |_|_|_| |_|\__, | # # |_| |___/ |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # from typing import List from unittest import TestCase from pyTooling.Warning import WarningCollector, Warning, CriticalWarning from pyTooling.Warning import UnhandledExceptionException, UnhandledCriticalWarningException if __name__ == "__main__": # pragma: no cover print("ERROR: you called a testcase declaration file as an executable module.") print("Use: 'python -m unittest '") exit(1) class ClassA: _x: object def __init__(self, x: object) -> None: self._x = x def methA_RaiseWarning(self) -> None: WarningCollector.Raise(Warning("Warning from ClassA.methA_RaiseWarning")) def methA_RaiseCriticalWarning(self) -> None: WarningCollector.Raise(CriticalWarning("CriticalWarning from ClassA.methA_RaiseCriticalWarning")) def methA_RaiseException(self) -> None: WarningCollector.Raise(Exception("Exception from ClassA.methA_RaiseException")) class ClassB: _a: ClassA def __init__(self, a: ClassA) -> None: self._a = a def methB(self) -> None: self._a.methA_RaiseException() class ClassC: _b: ClassB def __init__(self, b: ClassB) -> None: self._b = b def methC(self) -> None: self._b.methB() class Handler: _c: ClassC _warnings: List def __init__(self, c: ClassC) -> None: self._c = c self._warnings = [] def meth(self) -> None: with WarningCollector(self._warnings) as warning: self._c.methC() class NoWarningCollector(TestCase): def test_RaiseWarning(self) -> None: a = ClassA("none") a.methA_RaiseWarning() def test_RaiseCriticalWarning(self) -> None: a = ClassA("none") with self.assertRaises(UnhandledCriticalWarningException) as ex: a.methA_RaiseCriticalWarning() self.assertEqual("Unhandled Critical Warning: CriticalWarning from ClassA.methA_RaiseCriticalWarning", str(ex.exception)) def test_RaiseException(self) -> None: a = ClassA("none") with self.assertRaises(UnhandledExceptionException) as ex: a.methA_RaiseException() self.assertEqual("Unhandled Exception: Exception from ClassA.methA_RaiseException", str(ex.exception)) class WarningCollection(TestCase): def test_WarningCollector_NoList(self) -> None: a = ClassA("list") with WarningCollector() as warning: a.methA_RaiseException() self.assertEqual(1, len(warning)) self.assertEqual("Exception from ClassA.methA_RaiseException", str(warning[0])) def test_WarningCollector_List(self) -> None: warnings = [] a = ClassA("list") with WarningCollector(warnings) as warning: a.methA_RaiseException() self.assertEqual(1, len(warnings)) self.assertEqual("Exception from ClassA.methA_RaiseException", str(warnings[0])) def test_WarningCollector_Print(self) -> None: print() message = "" def func(warning: Warning) -> bool: nonlocal message message = str(warning) print(message) return False a = ClassA("print") with WarningCollector(handler=func) as warning: a.methA_RaiseException() self.assertEqual("Exception from ClassA.methA_RaiseException", message) def test_WarningCollector_Abort(self) -> None: print() message = "" def func(warning: Warning) -> bool: nonlocal message message = str(warning) print(message) return True a = ClassA("abort") with self.assertRaises(Exception) as ex: with WarningCollector(handler=func) as warning: a.methA_RaiseException() self.assertEqual("Exception from ClassA.methA_RaiseException", message) self.assertEqual("Warning: Exception from ClassA.methA_RaiseException", str(ex.exception)) class CallStack(TestCase): def test_Level_1(self) -> None: warnings = [] a = ClassA(1) with WarningCollector(warnings) as warning: a.methA_RaiseException() self.assertEqual(1, len(warnings)) self.assertEqual("Exception from ClassA.methA_RaiseException", str(warnings[0])) def test_Level_2(self) -> None: warnings = [] a = ClassA(2) b = ClassB(a) with WarningCollector(warnings) as warning: b.methB() self.assertEqual(1, len(warnings)) self.assertEqual("Exception from ClassA.methA_RaiseException", str(warnings[0])) def test_Level_3(self) -> None: warnings = [] a = ClassA(3) b = ClassB(a) c = ClassC(b) with WarningCollector(warnings) as warning: c.methC() self.assertEqual(1, len(warnings)) self.assertEqual("Exception from ClassA.methA_RaiseException", str(warnings[0])) def test_Nested(self) -> None: a = ClassA(1) with WarningCollector() as warning1: with WarningCollector() as warning2: a.methA_RaiseException() self.assertIs(warning1, warning2.Parent) self.assertEqual(1, len(warning2)) self.assertEqual("Exception from ClassA.methA_RaiseException", str(warning2[0])) class Catch(TestCase): def test_Inner(self) -> None: warnings = [] a = ClassA(3) b = ClassB(a) c = ClassC(b) h = Handler(c) with WarningCollector(warnings) as warning: h.meth() self.assertEqual(0, len(warnings)) self.assertEqual(1, len(h._warnings)) self.assertEqual("Exception from ClassA.methA_RaiseException", str(h._warnings[0])) pyTooling-8.11.0/tests/unit/__init__.py000066400000000000000000000067301513317154500200320ustar00rootroot00000000000000# ==================================================================================================================== # # _____ _ _ # # _ __ _ |_ _|__ ___ | (_)_ __ __ _ # # | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | # # | |_) | |_| || | (_) | (_) | | | | | | (_| | # # | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, | # # |_| |___/ |___/ # # ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # License: # # ==================================================================================================================== # # Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # """Unit tests for pyTooling.""" pyTooling-8.11.0/tests/unit/requirements.txt000066400000000000000000000006231513317154500212000ustar00rootroot00000000000000-r ../../requirements.txt # Coverage collection Coverage ~= 7.13 # Test Runner pytest ~= 9.0 pytest-cov ~= 7.0 # For pyTooling.Dependency.Python testing aiohttp >= 3.12 # aiohttp limited on MSYS2 to 3.12.x requests >= 2.32 # For pyTooling.Configuration.YAML testing ruamel.yaml ~= 0.18.0 # For pyTooling.Packaging testing setuptools >= 80.0 # For pyTooling.TerminalUI testing colorama ~= 0.4.6