From 57801b45e6b8a2efbe0daf60958c965abe317255 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 31 Oct 2025 20:54:15 +0100 Subject: [PATCH 01/13] wip --- .editorconfig | 13 ++ .github/workflows/ci.yml | 49 +++++++ .gitignore | 51 ++++++++ .pre-commit-config.yaml | 19 +++ .vscode/settings.json | 10 ++ CHANGELOG.md | 25 ++++ CODE_OF_CONDUCT.md | 5 + CONTRIBUTING.md | 38 ++++++ LICENSE | 201 ++++++++++++++++++++++++++++ README.md | 108 ++++++++++++++- pyproject.toml | 158 ++++++++++++++++++++++ src/score_tools/__init__.py | 13 ++ src/score_tools/py.typed | 0 src/score_tools/ruff/__init__.py | 5 + src/score_tools/ruff/cli.py | 140 ++++++++++++++++++++ src/score_tools/ruff/defaults.toml | 53 ++++++++ tests/test_ruff_wrapper.py | 110 ++++++++++++++++ uv.lock | 203 +++++++++++++++++++++++++++++ 18 files changed, 1199 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 pyproject.toml create mode 100644 src/score_tools/__init__.py create mode 100644 src/score_tools/py.typed create mode 100644 src/score_tools/ruff/__init__.py create mode 100644 src/score_tools/ruff/cli.py create mode 100644 src/score_tools/ruff/defaults.toml create mode 100644 tests/test_ruff_wrapper.py create mode 100644 uv.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..93f7fb1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig helps maintain consistent coding styles +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef8a9a3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install package and deps + run: | + python -m pip install --upgrade pip + python -m pip install -e '.[dev,ruff]' + - name: Run ruff check + run: ruff check src/ tests/ + - name: Run ruff format check + run: ruff format --check src/ tests/ + + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install package and test deps + run: | + python -m pip install --upgrade pip + python -m pip install -e '.[dev]' + - name: Run tests with coverage + run: pytest --cov=score_tools --cov-report=xml --cov-report=term + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9748a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# Pytest +.pytest_cache/ + +# Coverage +.coverage +.coverage.* +htmlcov/ +coverage.xml + +# Ruff cache +.ruff_cache/ + +# IDEs +.idea/ + +# Mac/Linux +.DS_Store +*.swp +*.swo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a75ecf7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +# See https://pre-commit.com for more information +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-added-large-files + - id: check-merge-conflict + - id: mixed-line-ending + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.7.4 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..eac1ff4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "ruff.path": [ + ".venv/bin/score-ruff" + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7a1fcdd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial release of score-tools +- Support for Python 3.12 and 3.13 +- Ruff wrapper with Eclipse S-CORE defaults + - Automatic config discovery (ruff.toml, .ruff.toml, pyproject.toml) + - Bundled Eclipse S-CORE Ruff defaults +- Installable via pip/pipx/uvx with extras: `[ruff]`, `[all]` +- CLI flags: `--score-config`, `--print-config`, `--help`, `--version` +- Type hints (PEP 561 py.typed marker) +- Comprehensive test suite with pytest +- CI/CD with GitHub Actions (multi-OS, multi-Python version) +- Pre-commit hooks configuration +- Apache-2.0 license +- Documentation (README, CONTRIBUTING, CODE_OF_CONDUCT) + +[Unreleased]: https://github.com/eclipse-score/tools/compare/...HEAD diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d47a9bb --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of Conduct + +This project adheres to the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e18bf84 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing to score-tools + +Thanks for your interest in contributing! This project is part of [Eclipse S-CORE](https://projects.eclipse.org/projects/automotive.score) and aims to provide thin wrappers around common developer tools with S-CORE defaults. + +## Getting started + +- Python 3.12 or 3.13 +- Create a virtual environment and install dev deps: + +```bash +python -m venv .venv && . .venv/bin/activate +pip install -e '.[dev,ruff]' +``` + +## Running tests + +```bash +pytest -q +``` + +## Making changes + +- Keep wrappers thin and predictable. +- Prefer conservative defaults. Project configs should always take precedence when present. +- Avoid hard dependencies in the base package; use extras like `[ruff]` and `[all]`. + +## Releasing + +- Bump `__version__` in `src/score_tools/__init__.py` and `pyproject.toml` as appropriate. +- Build and publish via your preferred workflow (e.g., `hatch build` or `python -m build` followed by `twine upload`). + +## Code of Conduct + +This project follows the Contributor Covenant. See `CODE_OF_CONDUCT.md`. + +## License + +By contributing to this project, you agree that your contributions will be licensed under the Apache License 2.0. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..992cd5a --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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. + + END OF TERMS AND CONDITIONS + + 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 2025 Eclipse S-CORE contributors + + 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. diff --git a/README.md b/README.md index fbff2dd..45aeb65 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,106 @@ -# tools -Home of score-tools, the new pypi based tools approach +# score-tools + +Eclipse S-CORE tool wrappers and defaults for a consistent, batteries-included developer experience. + +Today this includes: + +- score-ruff — a thin wrapper around Ruff that applies S-CORE defaults when no project config is present. + +Planned: additional wrappers following the same pattern. + +## Install + +Use one of the following: + +```bash +# With pip (extras to pull tools) +pip install 'score-tools[ruff]' + +# With pipx +pipx install 'score-tools[ruff]' + +# With uv +uv pip install 'score-tools[ruff]' + +# Run-once without installing (uvx) +# Note: use --from to include extras so Ruff is available +uvx --from 'score-tools[ruff]' score-ruff --version +``` + +Python 3.12–3.13 are supported. + +## Usage + +score-ruff will: + +- Use your project's config if it finds one (ruff.toml/.ruff.toml or pyproject.toml with [tool.ruff]). +- Otherwise, apply S-CORE's default Ruff configuration bundled in the package. + +Examples: + +```bash +# Lint the current directory with defaults if no config is present +score-ruff + +# Target a specific file or directory +score-ruff src/ + +# Print the bundled defaults +score-ruff --score-config + +# Print which config would be used +score-ruff --print-config + +# Show Ruff help through the wrapper +score-ruff --help +``` + +## Extras + +- ruff — installs Ruff alongside the wrapper +- all — currently an alias for ruff (future tools will be included here) + +Install specific tools via extras, e.g.: + +```bash +pipx install 'score-tools[ruff]' +``` + +## Development + +Clone the repository and set up your development environment: + +```bash +# Clone the repository +git clone https://github.com/eclipse-score/tools.git +cd tools + +# Create a virtual environment +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install in editable mode with dev dependencies +pip install -e '.[dev,ruff]' + +# Install pre-commit hooks (optional but recommended) +pip install pre-commit +pre-commit install + +# Run tests +pytest + +# Run tests with coverage +pytest --cov=score_tools --cov-report=html + +# Run linting +ruff check src/ tests/ +ruff format src/ tests/ + +# Build the package +pip install build +python -m build +``` + +## License + +This project is part of [Eclipse S-CORE](https://projects.eclipse.org/projects/automotive.score) and is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for details. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..68588ea --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,158 @@ +[build-system] +requires = ["hatchling>=1.25"] +build-backend = "hatchling.build" + +[project] +name = "score-tools" +dynamic = ["version"] +description = "Eclipse S-CORE tool wrappers and defaults (e.g., Ruff) for a consistent developer experience." +readme = "README.md" +requires-python = ">=3.12" +license = { file = "LICENSE" } +authors = [ + { name = "Alexander Lanin" } +] +keywords = ["score", "s-core", "eclipse"] +classifiers = [ + # Project maturity & audience + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + + # License + "License :: OSI Approved :: Apache Software License", + + # Environment & OS + "Environment :: Console", + "Operating System :: OS Independent", + + # Python versions & impls (keep in sync with requires-python) + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + + # Topics + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Utilities", + + # Typing + "Typing :: Typed", +] +# Runtime dependencies for the base package (keep minimal) +dependencies = [] + +[project.urls] +Homepage = "https://github.com/eclipse-score/tools" +Repository = "https://github.com/eclipse-score/tools" +"Bug Tracker" = "https://github.com/eclipse-score/tools/issues" +"Eclipse S-CORE" = "https://projects.eclipse.org/projects/automotive.score" + +[project.optional-dependencies] +# Install Ruff alongside the wrappers that need it +ruff = [ + "ruff>=0.6", +] +# Convenience extra to install everything +all = [ + "ruff>=0.6", +] +# Developer extras for working on this repository +dev = [ + "pytest>=8.0", + "pytest-cov>=5.0", +] + +[project.scripts] +score-ruff = "score_tools.ruff.cli:main" + +[tool.hatch.version] +path = "src/score_tools/__init__.py" + +[tool.hatch.build.targets.wheel] +packages = ["src/score_tools"] + +[tool.hatch.build] +# Include only the src package and essential top-level files +include = [ + "src/score_tools/**/*.py", + "src/score_tools/py.typed", + "src/score_tools/ruff/defaults.toml", + "README.md", + "LICENSE", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--showlocals", +] +pythonpath = ["src"] +filterwarnings = [ + "error", +] + +[tool.coverage.run] +source = ["score_tools"] +branch = true + +[tool.coverage.report] +show_missing = true +skip_covered = false +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "if __name__ == .__main__.:", + "raise NotImplementedError", + "raise AssertionError", + "@abstractmethod", +] + +# Ruff configuration for this repository +[tool.ruff] +src = ["src", "tests"] +line-length = 120 +target-version = "py312" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "RUF", # ruff-specific rules + "PT", # flake8-pytest-style + "PIE", # flake8-pie + "T20", # flake8-print + "RSE", # flake8-raise + "RET", # flake8-return +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.lint.isort] +known-first-party = ["score_tools"] +combine-as-imports = true + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "S101", # assert used + "T201", # print allowed in tests + "TCH", # no TYPE_CHECKING in tests +] + +[tool.uv] +package = true diff --git a/src/score_tools/__init__.py b/src/score_tools/__init__.py new file mode 100644 index 0000000..d5bd695 --- /dev/null +++ b/src/score_tools/__init__.py @@ -0,0 +1,13 @@ +"""score_tools + +Eclipse S-CORE tool wrappers and defaults. + +This package provides thin wrappers around common developer tools +with Eclipse S-CORE defaults, installable via optional extras (e.g., [ruff]). +""" + +__all__ = [ + "__version__", +] + +__version__ = "0.0.1" diff --git a/src/score_tools/py.typed b/src/score_tools/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/score_tools/ruff/__init__.py b/src/score_tools/ruff/__init__.py new file mode 100644 index 0000000..f383439 --- /dev/null +++ b/src/score_tools/ruff/__init__.py @@ -0,0 +1,5 @@ +"""score_tools.ruff subpackage - Ruff wrapper with Eclipse S-CORE defaults.""" + +from .cli import main + +__all__ = ["main"] diff --git a/src/score_tools/ruff/cli.py b/src/score_tools/ruff/cli.py new file mode 100644 index 0000000..fdff8ba --- /dev/null +++ b/src/score_tools/ruff/cli.py @@ -0,0 +1,140 @@ +"""Command-line interface for score-ruff (Ruff wrapper with Eclipse S-CORE defaults).""" + +from __future__ import annotations + +import argparse +import os +import sys +import tempfile +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterable + +try: + from importlib.resources import files as _pkg_files +except ImportError: # Python < 3.9 + from importlib_resources import files as _pkg_files # type: ignore[import-not-found,no-redef] + +from score_tools import __version__ + +RuffConfigPath = Path | None + + +def _find_user_ruff_config(start: Path) -> RuffConfigPath: + """Search upwards for a user Ruff config file.""" + toml_names = ("ruff.toml", ".ruff.toml") + for parent in [start, *start.parents]: + for name in toml_names: + candidate = parent / name + if candidate.is_file(): + return candidate + pyproject = parent / "pyproject.toml" + if pyproject.is_file(): + try: + text = pyproject.read_text(encoding="utf-8") + except Exception: + continue + if "[tool.ruff]" in text or "[tool.ruff.lint]" in text: + return pyproject + return None + + +def _write_temp_config(toml_text: str) -> Path: + """Write TOML text to a temporary file and return the path.""" + with tempfile.NamedTemporaryFile( + mode="w", + suffix=".toml", + prefix="score-ruff-", + delete=False, + encoding="utf-8", + ) as tmp: + tmp.write(toml_text) + tmp.flush() + return Path(tmp.name) + + +def _run_ruff(argv: Iterable[str]) -> int: + args = ["ruff", *argv] + try: + os.execvp("ruff", args) # type: ignore[attr-defined] + except FileNotFoundError: + sys.stderr.write( + "ruff is not installed. You need to explicitly select it during installation, e.g. with `pipx install 'score-tools[ruff]'` or `uvx --from 'score-tools[ruff]' score-ruff`.\n" + ) + return 127 + except Exception: + # Fallback: try module runner + try: + import runpy + + sys.argv = args + runpy.run_module("ruff", run_name="__main__") + return 0 + except Exception as exc: # pragma: no cover + sys.stderr.write(f"Failed to run ruff: {exc}\n") + return 1 + + +def main(argv: list[str] | None = None) -> int: + if argv is None: + argv = sys.argv[1:] + + p = argparse.ArgumentParser( + description="score-ruff: A wrapper around Ruff with Eclipse S-CORE defaults", + epilog="All other arguments are passed through to Ruff", + add_help=False, + ) + p.add_argument("-h", "--help", action="store_true", help="Show this help and Ruff's help, then exit") + p.add_argument("-v", "--version", action="store_true", help="Show score-ruff and Ruff versions, then exit") + p.add_argument("--config", type=str, help="Path to ruff.toml/.ruff.toml or pyproject.toml") + p.add_argument( + "--score-config", action="store_true", help="Print the bundled Eclipse S-CORE default config and exit" + ) + p.add_argument("--print-config", action="store_true", help="Print the resolved config (path) and exit") + + parsed, rest = p.parse_known_args(argv) + + if parsed.help: + print(p.format_help()) # noqa: T201 + print("\n--- Ruff help ---\n") # noqa: T201 + return _run_ruff(["--help"]) # exits + + if parsed.version: + print(f"score-ruff version {__version__}") # noqa: T201 + return _run_ruff(["--version"]) # exits + + if parsed.score_config: + try: + defaults_text = _pkg_files("score_tools.ruff").joinpath("defaults.toml").read_text(encoding="utf-8") + except Exception as exc: + sys.stderr.write(f"Failed to read bundled defaults: {exc}\n") + return 1 + print(defaults_text.strip()) # noqa: T201 + return 0 + + cwd = Path.cwd() + user_cfg_path: RuffConfigPath = Path(parsed.config) if parsed.config else _find_user_ruff_config(cwd) + + if parsed.print_config: + if user_cfg_path is None: + print("Using Eclipse S-CORE default Ruff configuration (no project config found).") # noqa: T201 + else: + print(f"Using project configuration at: {user_cfg_path}") # noqa: T201 + return 0 + + if user_cfg_path is None: + try: + defaults_text = _pkg_files("score_tools.ruff").joinpath("defaults.toml").read_text(encoding="utf-8") + except Exception as exc: + sys.stderr.write(f"Failed to read bundled defaults: {exc}\n") + return 1 + cfg_path = _write_temp_config(defaults_text) + return _run_ruff(["--config", str(cfg_path), *rest]) + + return _run_ruff(["--config", str(user_cfg_path), *rest]) + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/src/score_tools/ruff/defaults.toml b/src/score_tools/ruff/defaults.toml new file mode 100644 index 0000000..ed7523d --- /dev/null +++ b/src/score_tools/ruff/defaults.toml @@ -0,0 +1,53 @@ +# Eclipse S-CORE Ruff defaults + +line-length = 120 +indent-width = 4 + +[format] +quote-style = "double" +indent-style = "space" +docstring-code-format = true + +[lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM",# flake8-simplify + "RUF" # ruff-specific rules +] +ignore = [ + # Allow unused variables prefixed with `_` + "F401", # imported but unused (often managed by tooling) +] + +[lint.isort] +combine-as-imports = true +known-first-party = [] + +[lint.per-file-ignores] +"tests/**" = [ + "S101", # assert used +] + +[lint.pydocstyle] +convention = "pep257" + +[lint.flake8-quotes] +inline-quotes = "double" + +[lint.mccabe] +max-complexity = 12 + +[exclude] +extend = [ + "**/__pycache__", + "**/.venv", + "**/.tox", + "**/.pytest_cache", + "**/build", + "**/dist", + "**/.git", +] diff --git a/tests/test_ruff_wrapper.py b/tests/test_ruff_wrapper.py new file mode 100644 index 0000000..bdb3358 --- /dev/null +++ b/tests/test_ruff_wrapper.py @@ -0,0 +1,110 @@ +"""Tests for score-ruff wrapper.""" + +from __future__ import annotations + +from contextlib import redirect_stdout +from io import StringIO +from pathlib import Path + +import pytest + +from score_tools.ruff import cli as score_ruff + + +def run_and_capture(argv: list[str]) -> tuple[int, str]: + """Run score-ruff with argv and capture stdout.""" + buf = StringIO() + with redirect_stdout(buf): + code = score_ruff.main(argv) + return code, buf.getvalue() + + +def test_score_config_prints_defaults() -> None: + """Test that --score-config prints bundled defaults.""" + code, out = run_and_capture(["--score-config"]) + assert code == 0 + assert "[lint]" in out or "[format]" in out + assert "Eclipse S-CORE" in out + + +def test_print_config_without_user_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that --print-config reports using defaults when no user config is found.""" + # Run in a temp directory with no config present + monkeypatch.chdir(tmp_path) + code, out = run_and_capture(["--print-config"]) + assert code == 0 + assert "Eclipse S-CORE default Ruff configuration" in out + + +def test_print_config_with_pyproject(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that --print-config detects pyproject.toml with [tool.ruff].""" + # Create a minimal pyproject with [tool.ruff] + pyproj = tmp_path / "pyproject.toml" + pyproj.write_text( + """ +[tool.ruff] +line-length = 88 +""".strip(), + encoding="utf-8", + ) + monkeypatch.chdir(tmp_path) + + code, out = run_and_capture(["--print-config"]) + assert code == 0 + # Should report using the project configuration + assert str(pyproj) in out + + +def test_print_config_with_ruff_toml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that --print-config detects ruff.toml.""" + ruff_toml = tmp_path / "ruff.toml" + ruff_toml.write_text("line-length = 100\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + code, out = run_and_capture(["--print-config"]) + assert code == 0 + assert str(ruff_toml) in out + + +def test_print_config_with_dot_ruff_toml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that --print-config detects .ruff.toml.""" + ruff_toml = tmp_path / ".ruff.toml" + ruff_toml.write_text("line-length = 100\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + code, out = run_and_capture(["--print-config"]) + assert code == 0 + assert str(ruff_toml) in out + + +def test_find_config_in_parent(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that config search walks up parent directories.""" + # Create config in parent + pyproj = tmp_path / "pyproject.toml" + pyproj.write_text("[tool.ruff]\nline-length = 88\n", encoding="utf-8") + + # Run from subdirectory + subdir = tmp_path / "src" / "mypackage" + subdir.mkdir(parents=True) + monkeypatch.chdir(subdir) + + code, out = run_and_capture(["--print-config"]) + assert code == 0 + assert str(pyproj) in out + + +def test_explicit_config_flag(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that --config flag overrides config discovery.""" + # Create two configs + default_cfg = tmp_path / "ruff.toml" + default_cfg.write_text("line-length = 88\n", encoding="utf-8") + + custom_cfg = tmp_path / "custom-ruff.toml" + custom_cfg.write_text("line-length = 100\n", encoding="utf-8") + + monkeypatch.chdir(tmp_path) + + code, out = run_and_capture(["--print-config", "--config", str(custom_cfg)]) + assert code == 0 + assert str(custom_cfg) in out + assert str(default_cfg) not in out diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b7f1e54 --- /dev/null +++ b/uv.lock @@ -0,0 +1,203 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" }, + { url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" }, + { url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, +] + +[[package]] +name = "score-tools" +source = { editable = "." } + +[package.optional-dependencies] +all = [ + { name = "ruff" }, +] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] +ruff = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, + { name = "ruff", marker = "extra == 'all'", specifier = ">=0.6" }, + { name = "ruff", marker = "extra == 'ruff'", specifier = ">=0.6" }, +] +provides-extras = ["all", "dev", "ruff"] From 650b4f0008c262774d31fcaa296b5870c8468356 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sat, 1 Nov 2025 23:44:12 +0100 Subject: [PATCH 02/13] add score-tools command --- pyproject.toml | 2 + src/score_tools/cli.py | 148 ++++++++++++++++++++++++++++++++++ tests/test_score_tools_cli.py | 27 +++++++ 3 files changed, 177 insertions(+) create mode 100644 src/score_tools/cli.py create mode 100644 tests/test_score_tools_cli.py diff --git a/pyproject.toml b/pyproject.toml index 68588ea..70b68cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ dev = [ [project.scripts] score-ruff = "score_tools.ruff.cli:main" +score-tools = "score_tools.cli:main" [tool.hatch.version] path = "src/score_tools/__init__.py" @@ -141,6 +142,7 @@ select = [ ] ignore = [ "E501", # line too long (handled by formatter) + "T201", # allow print() globally for CLI friendliness ] [tool.ruff.lint.isort] diff --git a/src/score_tools/cli.py b/src/score_tools/cli.py new file mode 100644 index 0000000..c214565 --- /dev/null +++ b/src/score_tools/cli.py @@ -0,0 +1,148 @@ +""" +Eclipse S-CORE convenience CLI: "score-tools". + +Features: +- Lists known tool wrappers and their availability (Python package & CLI in PATH). +- Detects available package managers (uv, pipx, pip) and suggests install commands. +- Supports JSON output for scripting. + +Usage examples: +- score-tools +- score-tools --json +""" + +from __future__ import annotations + +import argparse +import json +import shutil +import sys +from dataclasses import asdict, dataclass +from importlib import metadata +from typing import Any + +from . import __version__ + + +def _is_cmd_available(cmd: str) -> bool: + return shutil.which(cmd) is not None + + +def _pkg_version(dist_name: str) -> str | None: + try: + return metadata.version(dist_name) + except metadata.PackageNotFoundError: + return None + + +@dataclass +class ToolStatus: + name: str + python: bool + cli: bool + + +def _detect_tool_score_ruff() -> ToolStatus: + # This is our wrapper CLI; version matches score-tools package version + return ToolStatus( + name="score-ruff", + python=True, # If this CLI runs, score-tools is importable + cli=_is_cmd_available("score-ruff"), + ) + + +def _available_package_managers() -> list[str]: + managers: list[str] = [] + if _is_cmd_available("uv"): + managers.append("uv") + if _is_cmd_available("pipx"): + managers.append("pipx") + if _is_cmd_available("pip"): + managers.append("pip") + return managers + + +def _suggest_installs(managers: list[str]) -> dict[str, list[str]]: + # Provide suggestions for installing extras and running CLIs without activation + suggestions: dict[str, list[str]] = {} + + if "uv" in managers: + suggestions["uv"] = [ + "uv pip install 'score-tools[all]'", + "uv run score-ruff --version", + "uvx score-ruff --version", + ] + if "pipx" in managers: + suggestions["pipx"] = [ + "pipx install 'score-tools[all]'", + "score-ruff --version", + ] + if "pip" in managers: + suggestions["pip"] = [ + "python -m pip install 'score-tools[all]'", + ".venv/bin/activate", + "score-ruff --version", + ] + return suggestions + + +def build_status() -> dict[str, Any]: + tools = [ + _detect_tool_score_ruff(), + ] + managers = _available_package_managers() + + return { + "score_tools_version": __version__, + "tools": {t.name: asdict(t) for t in tools}, + "package_managers": managers, + "suggest": _suggest_installs(managers), + } + + +def _pretty_print_status(status: dict[str, Any]) -> None: + print(f"score-tools {status['score_tools_version']}") + print("") + print("Embedded tools:") + for name, data in status["tools"].items(): + installed = "yes" if (data.get("python") or data.get("cli")) else "no" + print(f" - {name:11} installed: {installed:3}") + + print("") + if status["package_managers"]: + print("Detected package managers: " + ", ".join(status["package_managers"])) + else: + print("No package managers detected in PATH.") + + if status["suggest"]: + print("") + print("Install and usage suggestions:") + for mgr, cmds in status["suggest"].items(): + print(f" [{mgr}]") + for cmd in cmds: + print(f" $ {cmd}") + + +def main(argv: list[str] | None = None) -> int: # pragma: no cover - thin wrapper + parser = argparse.ArgumentParser( + prog="score-tools", + description="Eclipse S-CORE convenience CLI: list tools and show install tips.", + ) + parser.add_argument( + "--json", + action="store_true", + help="Output as JSON for scripting.", + ) + args = parser.parse_args(argv) + + status = build_status() + if args.json: + sys.stdout.write(json.dumps(status, indent=2) + "\n") + else: + _pretty_print_status(status) + + return 0 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/tests/test_score_tools_cli.py b/tests/test_score_tools_cli.py new file mode 100644 index 0000000..53476a7 --- /dev/null +++ b/tests/test_score_tools_cli.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import json + +import score_tools +from score_tools import cli as score_cli + + +def test_score_tools_json_structure(capsys) -> None: + code = score_cli.main(["--json"]) + assert code == 0 + out = capsys.readouterr().out + data = json.loads(out) + assert data["score_tools_version"] == score_tools.__version__ + # Known tools present + tools = data["tools"] + assert "score-ruff" in tools + # Package managers is a list (may be empty on CI) + assert isinstance(data["package_managers"], list) + + +def test_score_tools_pretty_runs(capsys) -> None: + code = score_cli.main([]) + assert code == 0 + out = capsys.readouterr().out + assert "tools:" in out + assert "score-ruff" in out From 104c90688df99a9bb876b7eddcd31129345f4405 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sat, 1 Nov 2025 23:49:17 +0100 Subject: [PATCH 03/13] add black box tests --- .vscode/settings.json | 3 -- src/score_tools/ruff/cli.py | 16 +++++---- tests/test_cli_blackbox.py | 65 +++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 tests/test_cli_blackbox.py diff --git a/.vscode/settings.json b/.vscode/settings.json index eac1ff4..d7338ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,4 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "ruff.path": [ - ".venv/bin/score-ruff" - ] } \ No newline at end of file diff --git a/src/score_tools/ruff/cli.py b/src/score_tools/ruff/cli.py index fdff8ba..46874c4 100644 --- a/src/score_tools/ruff/cli.py +++ b/src/score_tools/ruff/cli.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import contextlib import os import sys import tempfile @@ -58,6 +59,9 @@ def _write_temp_config(toml_text: str) -> Path: def _run_ruff(argv: Iterable[str]) -> int: args = ["ruff", *argv] try: + # Ensure any prior prints are visible before replacing the process. + with contextlib.suppress(Exception): + sys.stdout.flush() os.execvp("ruff", args) # type: ignore[attr-defined] except FileNotFoundError: sys.stderr.write( @@ -97,12 +101,12 @@ def main(argv: list[str] | None = None) -> int: parsed, rest = p.parse_known_args(argv) if parsed.help: - print(p.format_help()) # noqa: T201 - print("\n--- Ruff help ---\n") # noqa: T201 + print(p.format_help()) + print("\n--- Ruff help ---\n") return _run_ruff(["--help"]) # exits if parsed.version: - print(f"score-ruff version {__version__}") # noqa: T201 + print(f"score-ruff version {__version__}") return _run_ruff(["--version"]) # exits if parsed.score_config: @@ -111,7 +115,7 @@ def main(argv: list[str] | None = None) -> int: except Exception as exc: sys.stderr.write(f"Failed to read bundled defaults: {exc}\n") return 1 - print(defaults_text.strip()) # noqa: T201 + print(defaults_text.strip()) return 0 cwd = Path.cwd() @@ -119,9 +123,9 @@ def main(argv: list[str] | None = None) -> int: if parsed.print_config: if user_cfg_path is None: - print("Using Eclipse S-CORE default Ruff configuration (no project config found).") # noqa: T201 + print("Using Eclipse S-CORE default Ruff configuration (no project config found).") else: - print(f"Using project configuration at: {user_cfg_path}") # noqa: T201 + print(f"Using project configuration at: {user_cfg_path}") return 0 if user_cfg_path is None: diff --git a/tests/test_cli_blackbox.py b/tests/test_cli_blackbox.py new file mode 100644 index 0000000..e0c5c50 --- /dev/null +++ b/tests/test_cli_blackbox.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +def run_module(mod: str, *args: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "-m", mod, *args], + cwd=str(cwd) if cwd else None, + text=True, + capture_output=True, + check=False, + ) + + +def test_score_tools_cli_pretty_blackbox() -> None: + proc = run_module("score_tools.cli") + assert proc.returncode == 0 + assert "score-tools" in proc.stdout + assert ("Embedded tools:" in proc.stdout) or ("Detected tools:" in proc.stdout) + assert "score-ruff" in proc.stdout + + +def test_score_tools_cli_json_blackbox() -> None: + proc = run_module("score_tools.cli", "--json") + assert proc.returncode == 0 + data = json.loads(proc.stdout) + assert "score_tools_version" in data + assert "tools" in data + assert isinstance(data["tools"], dict) + assert "score-ruff" in data["tools"] + + +def test_score_ruff_cli_version_blackbox() -> None: + proc = run_module("score_tools.ruff.cli", "--version") + # We always print our wrapper version; ruff may not be installed + assert "score-ruff version" in proc.stdout + assert proc.returncode in (0, 127) + if proc.returncode == 127: + assert "ruff is not installed" in proc.stderr + + +def test_score_ruff_cli_score_config_blackbox() -> None: + proc = run_module("score_tools.ruff.cli", "--score-config") + assert proc.returncode == 0 + assert "S-CORE Ruff defaults" in proc.stdout + # Basic shape of a ruff config + assert "[lint]" in proc.stdout or "line-length" in proc.stdout + + +def test_score_ruff_cli_print_config_default_blackbox(tmp_path: Path) -> None: + proc = run_module("score_tools.ruff.cli", "--print-config", cwd=tmp_path) + assert proc.returncode == 0 + assert "default Ruff configuration" in proc.stdout + + +def test_score_ruff_cli_print_config_project_blackbox(tmp_path: Path) -> None: + pyproj = tmp_path / "pyproject.toml" + pyproj.write_text("[tool.ruff]\nline-length = 88\n", encoding="utf-8") + proc = run_module("score_tools.ruff.cli", "--print-config", cwd=tmp_path) + assert proc.returncode == 0 + assert str(pyproj) in proc.stdout From d58b868b3896ab29869448c40ac18d67737fe5d9 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 00:53:03 +0100 Subject: [PATCH 04/13] try test job --- .github/workflows/installer-and-tests.yml | 170 ++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/workflows/installer-and-tests.yml diff --git a/.github/workflows/installer-and-tests.yml b/.github/workflows/installer-and-tests.yml new file mode 100644 index 0000000..8043145 --- /dev/null +++ b/.github/workflows/installer-and-tests.yml @@ -0,0 +1,170 @@ +name: CI + +on: + push: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + outputs: + sdist: ${{ steps.paths.outputs.sdist }} + wheel: ${{ steps.paths.outputs.wheel }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Build + run: | + # Build sdist and wheel artifacts from the checked-out source. + # Keep this job minimal and deterministic. + python -m pip install -U pip build + python -m build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/* + + - id: paths + run: | + # Record artifact paths for downstream jobs (convenience only). + echo "sdist=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" + echo "wheel=$(ls dist/*.whl | head -n1)" >> "$GITHUB_OUTPUT" + + tests: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python: ["3.10","3.11","3.12","3.13"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + cache: pip + + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist } + + - uses: astral-sh/setup-uv@v7 + + - name: Install from wheel + dev deps + run: | + # Prefer uv for speed; the tool was set up in the previous step. + # Install the built wheel so tests import the installed artifact, not the repo's src/. + uv pip install dist/*.whl + uv pip install 'pytest>=8' 'ruff>=0.6' 'mypy>=1.10' 'coverage[toml]>=7' + + - run: python -m ruff check src tests + + - run: python -m mypy src + + - run: | + mkdir -p reports + + # Run tests against the installed wheel, not the source tree's `src`. + # `-c /dev/null` ignores repo pytest config (e.g., pythonpath=['src']). + python -m coverage run -m pytest -q -c /dev/null tests --junitxml=reports/junit.xml + + # Export coverage for upload. + python -m coverage xml -o reports/coverage.xml + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: reports-${{ matrix.os }}-${{ matrix.python }} + path: reports + + installer-uvx: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist } + + - uses: astral-sh/setup-uv@v7 + + - name: uvx smoke + run: | + # Validate the CLI runs when installed via uvx directly from the wheel. + uvx --from dist/*.whl score-tools --help + + installer-pipx: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist } + + - name: pipx smoke + run: | + # Validate the CLI installs and runs via pipx. + # We invoke pipx via `python -m` to avoid PATH issues. + python -m pip install -U pip pipx + python -m pipx install dist/*.whl + python -m pipx run score-tools --help + + installer-pip: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist } + + - name: pip smoke + run: | + # Validate the CLI installs and runs via pip. + # On Windows, invoking the console script can be finicky; calling module main is robust. + python -m pip install -U pip + python -m pip install dist/*.whl + python -c "import score_tools.cli as c; import sys; sys.exit(c.main([]))" From 2482c160dd0db13fa071367d0b006b26d4f5837d Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 01:05:31 +0100 Subject: [PATCH 05/13] fix some issues --- .github/workflows/installer-and-tests.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/installer-and-tests.yml b/.github/workflows/installer-and-tests.yml index 8043145..2140360 100644 --- a/.github/workflows/installer-and-tests.yml +++ b/.github/workflows/installer-and-tests.yml @@ -69,8 +69,8 @@ jobs: run: | # Prefer uv for speed; the tool was set up in the previous step. # Install the built wheel so tests import the installed artifact, not the repo's src/. - uv pip install dist/*.whl - uv pip install 'pytest>=8' 'ruff>=0.6' 'mypy>=1.10' 'coverage[toml]>=7' + uv pip install --system "${{ needs.build.outputs.wheel }}" + uv pip install --system 'pytest>=8' 'ruff>=0.6' 'mypy>=1.10' 'coverage[toml]>=7' - run: python -m ruff check src tests @@ -115,7 +115,7 @@ jobs: - name: uvx smoke run: | # Validate the CLI runs when installed via uvx directly from the wheel. - uvx --from dist/*.whl score-tools --help + uvx --from "${{ needs.build.outputs.wheel }}" score-tools --help installer-pipx: needs: build @@ -140,7 +140,7 @@ jobs: # Validate the CLI installs and runs via pipx. # We invoke pipx via `python -m` to avoid PATH issues. python -m pip install -U pip pipx - python -m pipx install dist/*.whl + python -m pipx install "${{ needs.build.outputs.wheel }}" python -m pipx run score-tools --help installer-pip: @@ -166,5 +166,5 @@ jobs: # Validate the CLI installs and runs via pip. # On Windows, invoking the console script can be finicky; calling module main is robust. python -m pip install -U pip - python -m pip install dist/*.whl + python -m pip install "${{ needs.build.outputs.wheel }}" python -c "import score_tools.cli as c; import sys; sys.exit(c.main([]))" From 9c5de7338c67de0730e08016f2e7f7f707cd495f Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 01:14:39 +0100 Subject: [PATCH 06/13] support python >=3.10 --- .github/workflows/installer-and-tests.yml | 3 ++- pyproject.toml | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/installer-and-tests.yml b/.github/workflows/installer-and-tests.yml index 2140360..2473de3 100644 --- a/.github/workflows/installer-and-tests.yml +++ b/.github/workflows/installer-and-tests.yml @@ -58,7 +58,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - cache: pip + # Caching will fail if there is nothing to cache + #cache: pip - uses: actions/download-artifact@v4 with: { name: dist, path: dist } diff --git a/pyproject.toml b/pyproject.toml index 70b68cb..d177b18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "score-tools" dynamic = ["version"] description = "Eclipse S-CORE tool wrappers and defaults (e.g., Ruff) for a consistent developer experience." readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.10" license = { file = "LICENSE" } authors = [ { name = "Alexander Lanin" } @@ -29,6 +29,8 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", @@ -118,7 +120,7 @@ exclude_lines = [ [tool.ruff] src = ["src", "tests"] line-length = 120 -target-version = "py312" +target-version = "py310" [tool.ruff.lint] select = [ From 9ce7afec9548d8eba732c3e8d00caa445f66f47e Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 01:24:41 +0100 Subject: [PATCH 07/13] fix pipx --- .github/workflows/installer-and-tests.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/installer-and-tests.yml b/.github/workflows/installer-and-tests.yml index 2473de3..7da2b11 100644 --- a/.github/workflows/installer-and-tests.yml +++ b/.github/workflows/installer-and-tests.yml @@ -87,11 +87,7 @@ jobs: # Export coverage for upload. python -m coverage xml -o reports/coverage.xml - - uses: actions/upload-artifact@v4 - if: always() - with: - name: reports-${{ matrix.os }}-${{ matrix.python }} - path: reports + # TODO: do something with reports? run coverage only for one system? installer-uvx: needs: build @@ -138,11 +134,10 @@ jobs: - name: pipx smoke run: | - # Validate the CLI installs and runs via pipx. - # We invoke pipx via `python -m` to avoid PATH issues. + # Validate the CLI runs via pipx using an ephemeral environment from the built wheel. + # This avoids publishing requirements and extra PATH handling. python -m pip install -U pip pipx - python -m pipx install "${{ needs.build.outputs.wheel }}" - python -m pipx run score-tools --help + python -m pipx run --spec "${{ needs.build.outputs.wheel }}" score-tools --help installer-pip: needs: build From 8cee6b28051f5a59af38b9284730fb72aa06da2d Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 01:33:16 +0100 Subject: [PATCH 08/13] add badge --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 45aeb65..a583890 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # score-tools +[![CI](https://github.com/eclipse-score/tools/actions/workflows/installer-and-tests.yml/badge.svg?branch=main)](https://github.com/eclipse-score/tools/actions/workflows/installer-and-tests.yml) + Eclipse S-CORE tool wrappers and defaults for a consistent, batteries-included developer experience. Today this includes: @@ -27,7 +29,7 @@ uv pip install 'score-tools[ruff]' uvx --from 'score-tools[ruff]' score-ruff --version ``` -Python 3.12–3.13 are supported. +Python 3.10–3.13 are supported. ## Usage From 1c15b25d895cd0c12e710a11781fbfc04b88fd29 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 01:40:58 +0100 Subject: [PATCH 09/13] fix tool status reporting --- src/score_tools/cli.py | 7 +-- uv.lock | 106 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/src/score_tools/cli.py b/src/score_tools/cli.py index c214565..28b5e4c 100644 --- a/src/score_tools/cli.py +++ b/src/score_tools/cli.py @@ -43,11 +43,12 @@ class ToolStatus: def _detect_tool_score_ruff() -> ToolStatus: - # This is our wrapper CLI; version matches score-tools package version + # Check if the underlying tool (ruff) is available, not just our wrapper. + # The wrapper is only useful if ruff itself is installed via the [ruff] extra. return ToolStatus( name="score-ruff", - python=True, # If this CLI runs, score-tools is importable - cli=_is_cmd_available("score-ruff"), + python=_pkg_version("ruff") is not None, + cli=_is_cmd_available("ruff"), ) diff --git a/uv.lock b/uv.lock index b7f1e54..d7c27fa 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.12" +requires-python = ">=3.10" [[package]] name = "colorama" @@ -17,6 +17,31 @@ version = "7.11.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31", size = 215800, upload-time = "2025-10-15T15:12:19.824Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075", size = 216198, upload-time = "2025-10-15T15:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab", size = 242953, upload-time = "2025-10-15T15:12:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0", size = 244766, upload-time = "2025-10-15T15:12:25.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785", size = 246625, upload-time = "2025-10-15T15:12:27.387Z" }, + { url = "https://files.pythonhosted.org/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591", size = 243568, upload-time = "2025-10-15T15:12:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088", size = 244665, upload-time = "2025-10-15T15:12:30.297Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f", size = 242681, upload-time = "2025-10-15T15:12:32.326Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866", size = 242912, upload-time = "2025-10-15T15:12:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841", size = 243559, upload-time = "2025-10-15T15:12:35.809Z" }, + { url = "https://files.pythonhosted.org/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf", size = 218266, upload-time = "2025-10-15T15:12:37.429Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969", size = 219169, upload-time = "2025-10-15T15:12:39.25Z" }, + { url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" }, + { url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" }, + { url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" }, + { url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" }, + { url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" }, + { url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" }, { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, @@ -85,6 +110,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, ] +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -127,10 +169,12 @@ version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -142,7 +186,7 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage" }, + { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] @@ -201,3 +245,61 @@ requires-dist = [ { name = "ruff", marker = "extra == 'ruff'", specifier = ">=0.6" }, ] provides-extras = ["all", "dev", "ruff"] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 8d48d65d96a8ab2502e0138197b3def4777df1d3 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 01:50:49 +0100 Subject: [PATCH 10/13] refactor --- README.md | 26 +------------------------- src/score_tools/ruff/cli.py | 33 +++++++++++++++++++-------------- 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index a583890..9c44dc6 100644 --- a/README.md +++ b/README.md @@ -73,34 +73,10 @@ pipx install 'score-tools[ruff]' Clone the repository and set up your development environment: ```bash -# Clone the repository -git clone https://github.com/eclipse-score/tools.git -cd tools - -# Create a virtual environment python -m venv .venv +pip install -e '.[dev,all]' source .venv/bin/activate # On Windows: .venv\Scripts\activate - -# Install in editable mode with dev dependencies -pip install -e '.[dev,ruff]' - -# Install pre-commit hooks (optional but recommended) -pip install pre-commit -pre-commit install - -# Run tests pytest - -# Run tests with coverage -pytest --cov=score_tools --cov-report=html - -# Run linting -ruff check src/ tests/ -ruff format src/ tests/ - -# Build the package -pip install build -python -m build ``` ## License diff --git a/src/score_tools/ruff/cli.py b/src/score_tools/ruff/cli.py index 46874c4..fb8d271 100644 --- a/src/score_tools/ruff/cli.py +++ b/src/score_tools/ruff/cli.py @@ -13,16 +13,22 @@ if TYPE_CHECKING: from collections.abc import Iterable -try: - from importlib.resources import files as _pkg_files -except ImportError: # Python < 3.9 - from importlib_resources import files as _pkg_files # type: ignore[import-not-found,no-redef] +from importlib.resources import files as _pkg_files from score_tools import __version__ RuffConfigPath = Path | None +def _load_bundled_ruff_defaults() -> str: + """Load the bundled Eclipse S-CORE Ruff defaults from the package.""" + try: + return _pkg_files("score_tools.ruff").joinpath("defaults.toml").read_text(encoding="utf-8") + except Exception as exc: + sys.stderr.write(f"Failed to read bundled defaults: {exc}\n") + raise + + def _find_user_ruff_config(start: Path) -> RuffConfigPath: """Search upwards for a user Ruff config file.""" toml_names = ("ruff.toml", ".ruff.toml") @@ -110,13 +116,13 @@ def main(argv: list[str] | None = None) -> int: return _run_ruff(["--version"]) # exits if parsed.score_config: + # print bundled defaults try: - defaults_text = _pkg_files("score_tools.ruff").joinpath("defaults.toml").read_text(encoding="utf-8") - except Exception as exc: - sys.stderr.write(f"Failed to read bundled defaults: {exc}\n") + defaults_text = _load_bundled_ruff_defaults() + print(defaults_text.strip()) + return 0 + except Exception: return 1 - print(defaults_text.strip()) - return 0 cwd = Path.cwd() user_cfg_path: RuffConfigPath = Path(parsed.config) if parsed.config else _find_user_ruff_config(cwd) @@ -130,12 +136,11 @@ def main(argv: list[str] | None = None) -> int: if user_cfg_path is None: try: - defaults_text = _pkg_files("score_tools.ruff").joinpath("defaults.toml").read_text(encoding="utf-8") - except Exception as exc: - sys.stderr.write(f"Failed to read bundled defaults: {exc}\n") + defaults_text = _load_bundled_ruff_defaults() + cfg_path = _write_temp_config(defaults_text) + return _run_ruff(["--config", str(cfg_path), *rest]) + except Exception: return 1 - cfg_path = _write_temp_config(defaults_text) - return _run_ruff(["--config", str(cfg_path), *rest]) return _run_ruff(["--config", str(user_cfg_path), *rest]) From 4c6345d35faf548837caed2407c105f83ad64835 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 02:38:21 +0100 Subject: [PATCH 11/13] improve tool detection utilities --- pyproject.toml | 16 ++++- src/score_tools/cli.py | 25 +------ src/score_tools/ruff/cli.py | 100 ++++++++++++++++++++++++++- src/score_tools/utils.py | 131 ++++++++++++++++++++++++++++++++++++ uv.lock | 17 +++++ 5 files changed, 263 insertions(+), 26 deletions(-) create mode 100644 src/score_tools/utils.py diff --git a/pyproject.toml b/pyproject.toml index d177b18..4537839 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,14 +53,26 @@ Repository = "https://github.com/eclipse-score/tools" "Eclipse S-CORE" = "https://projects.eclipse.org/projects/automotive.score" [project.optional-dependencies] + # Install Ruff alongside the wrappers that need it ruff = [ - "ruff>=0.6", + "ruff==0.14.3", + + # No need to pin helper libraries + "tomli>=2.0.1; python_version < '3.11'", + "tomli-w>=1.0.0", ] + # Convenience extra to install everything all = [ - "ruff>=0.6", + # Exact versions of tools pinned + "ruff==0.14.3", + + # No need to pin helper libraries + "tomli>=2.0.1; python_version < '3.11'", + "tomli-w>=1.0.0", ] + # Developer extras for working on this repository dev = [ "pytest>=8.0", diff --git a/src/score_tools/cli.py b/src/score_tools/cli.py index 28b5e4c..8b0c5fc 100644 --- a/src/score_tools/cli.py +++ b/src/score_tools/cli.py @@ -18,38 +18,19 @@ import shutil import sys from dataclasses import asdict, dataclass -from importlib import metadata from typing import Any from . import __version__ +from .utils import ToolStatus, is_installed def _is_cmd_available(cmd: str) -> bool: return shutil.which(cmd) is not None -def _pkg_version(dist_name: str) -> str | None: - try: - return metadata.version(dist_name) - except metadata.PackageNotFoundError: - return None - - -@dataclass -class ToolStatus: - name: str - python: bool - cli: bool - def _detect_tool_score_ruff() -> ToolStatus: - # Check if the underlying tool (ruff) is available, not just our wrapper. - # The wrapper is only useful if ruff itself is installed via the [ruff] extra. - return ToolStatus( - name="score-ruff", - python=_pkg_version("ruff") is not None, - cli=_is_cmd_available("ruff"), - ) + return is_installed("ruff") def _available_package_managers() -> list[str]: @@ -106,7 +87,7 @@ def _pretty_print_status(status: dict[str, Any]) -> None: print("") print("Embedded tools:") for name, data in status["tools"].items(): - installed = "yes" if (data.get("python") or data.get("cli")) else "no" + installed = "yes" if data.get("installed") else "no" print(f" - {name:11} installed: {installed:3}") print("") diff --git a/src/score_tools/ruff/cli.py b/src/score_tools/ruff/cli.py index fb8d271..2b68f7e 100644 --- a/src/score_tools/ruff/cli.py +++ b/src/score_tools/ruff/cli.py @@ -62,6 +62,85 @@ def _write_temp_config(toml_text: str) -> Path: return Path(tmp.name) +def _loads_toml(text: str) -> dict: + # Lazy-import toml readers to avoid hard dependency unless ruff extra is installed. + try: # Python 3.11+ + import tomllib as _tomllib # type: ignore[attr-defined] + except Exception: + try: + import tomli as _tomllib # type: ignore[import-not-found] + except Exception as exc: + raise RuntimeError( + "TOML reader not available. Install 'score-tools[ruff]' to enable merged config support." + ) from exc + return _tomllib.loads(text) + + +def _load_toml_file(path: Path) -> dict: + try: # Python 3.11+ + import tomllib as _tomllib # type: ignore[attr-defined] + except Exception: + try: + import tomli as _tomllib # type: ignore[import-not-found] + except Exception as exc: + raise RuntimeError( + "TOML reader not available. Install 'score-tools[ruff]' to enable merged config support." + ) from exc + with path.open("rb") as f: + return _tomllib.load(f) + + +def _extract_ruff_table(data: dict) -> dict: + """Return a ruff-level config mapping regardless of file style. + + - If given a pyproject-style mapping with [tool.ruff], return that table. + - Otherwise, assume it's already ruff.toml style (top-level settings). + """ + tool = data.get("tool") if isinstance(data, dict) else None + if isinstance(tool, dict) and isinstance(tool.get("ruff"), dict): + return tool["ruff"] # type: ignore[return-value] + return data + + +def _deep_merge(base: dict, overrides: dict) -> dict: + """Deep-merge two dicts: values in overrides replace those in base. + + Lists and scalars are replaced entirely; nested dicts are merged recursively. + """ + out: dict = dict(base) + for k, v in overrides.items(): + if k in out and isinstance(out[k], dict) and isinstance(v, dict): + out[k] = _deep_merge(out[k], v) + else: + out[k] = v + return out + + +def _write_merged_config(defaults_text: str, user_cfg_path: Path) -> Path: + """Merge bundled defaults with user config and write a temp ruff.toml. + + The merged file is written in ruff.toml style (not pyproject), with user values + taking precedence over bundled defaults. + """ + defaults = _extract_ruff_table(_loads_toml(defaults_text)) + user_all = _load_toml_file(user_cfg_path) + user = _extract_ruff_table(user_all) + merged = _deep_merge(defaults, user) + + # Lazy import writer; if unavailable, signal to caller. + try: + import tomli_w # type: ignore[import-not-found] + except Exception as exc: + raise RuntimeError( + "TOML writer not available. Install 'score-tools[ruff]' to enable merged config support." + ) from exc + + with tempfile.NamedTemporaryFile(mode="wb", suffix=".toml", prefix="score-ruff-merged-", delete=False) as tmp: + tomli_w.dump(merged, tmp) + tmp.flush() + return Path(tmp.name) + + def _run_ruff(argv: Iterable[str]) -> int: args = ["ruff", *argv] try: @@ -131,7 +210,10 @@ def main(argv: list[str] | None = None) -> int: if user_cfg_path is None: print("Using Eclipse S-CORE default Ruff configuration (no project config found).") else: - print(f"Using project configuration at: {user_cfg_path}") + print( + "Using merged configuration: S-CORE defaults overlaid with project configuration at: " + f"{user_cfg_path}" + ) return 0 if user_cfg_path is None: @@ -142,7 +224,21 @@ def main(argv: list[str] | None = None) -> int: except Exception: return 1 - return _run_ruff(["--config", str(user_cfg_path), *rest]) + # Merge defaults with user config (user overrides defaults). If TOML libs are + # unavailable (base install without [ruff]), fail with a clear instruction so + # behavior is deterministic and easy to diagnose. + try: + defaults_text = _load_bundled_ruff_defaults() + merged_cfg = _write_merged_config(defaults_text, user_cfg_path) + return _run_ruff(["--config", str(merged_cfg), *rest]) + except Exception as exc: + sys.stderr.write( + "Failed to merge S-CORE defaults with project config.\n" + f"Reason: {exc}\n" + "Install the ruff extra to enable merging: `pipx install 'score-tools[ruff]'`\n" + "or `uv pip install 'score-tools[ruff]'`." + ) + return 2 if __name__ == "__main__": # pragma: no cover diff --git a/src/score_tools/utils.py b/src/score_tools/utils.py new file mode 100644 index 0000000..5c9e1f2 --- /dev/null +++ b/src/score_tools/utils.py @@ -0,0 +1,131 @@ +"""Common helper utilities for tool detection and version checks. + +These functions intentionally avoid hard dependencies. Where richer parsing is +useful (PEP 440 specifiers), we try to import `packaging` and fall back to +conservative behavior if it's not available. +""" + +from __future__ import annotations + +import shutil +from dataclasses import dataclass +from importlib import metadata + + +def is_cli_available(cmd: str) -> bool: + """Return True if a command is found in PATH.""" + return shutil.which(cmd) is not None + + +def dist_version(dist_name: str) -> str | None: + """Return installed distribution version, or None if not installed.""" + try: + return metadata.version(dist_name) + except metadata.PackageNotFoundError: + return None + + +def version_satisfies(dist_name: str, specifier: str) -> tuple[bool, str | None, str | None]: + """Check whether an installed dist satisfies a version specifier. + + Returns (ok, installed_version, reason_if_not_ok). + + - If the distribution is not installed, returns (False, None, "not installed"). + - If `packaging` is available, uses SpecifierSet for accurate evaluation. + - Without `packaging`, supports only exact equality (==) checks; otherwise + returns (True, version, None) to avoid false negatives. + """ + ver = dist_version(dist_name) + if ver is None: + return False, None, "not installed" + + try: + from packaging.specifiers import SpecifierSet # type: ignore + from packaging.version import Version # type: ignore + + ok = Version(ver) in SpecifierSet(specifier) + return (ok, ver, None if ok else f"{ver} does not satisfy '{specifier}'") + except Exception: + # Fallback: support only '==X' exact match; otherwise assume ok. + spec = specifier.strip() + if spec.startswith("=="): + expected = spec[2:].strip() + ok = ver == expected + return (ok, ver, None if ok else f"{ver} != {expected}") + return True, ver, None + + +@dataclass +class ToolStatus: + name: str + installed: bool # fully ready to use (all required extras installed) + details: dict[str, dict[str, object]] + + +def is_installed(tool: str) -> ToolStatus: + """Generic check that all requirements for an extra are satisfied. + + Returns (all_ok, details) where details maps requirement names to: + {"required": str, "installed": str|None, "ok": bool, "reason": str|None} + + This avoids hard-coding dependency names by reading the installed + distribution's "Requires-Dist" entries and selecting those tied to the + 'ruff' extra (respecting environment markers like python_version). + """ + try: + from packaging.markers import default_environment # type: ignore + from packaging.requirements import Requirement # type: ignore + except Exception: + return ToolStatus( + name=tool, + installed=False, + details={ + "packaging": { + "required": "packaging", + "installed": None, + "ok": False, + "reason": "packaging not available to parse extras; install 'packaging' to enable this check", + } + }, + ) + + dist = metadata.distribution("score-tools") + requires = dist.requires or [] + env: dict[str, str] = {k: str(v) for k, v in default_environment().items()} + env["extra"] = str(tool) + + selected: list[Requirement] = [] + for req_str in requires: + try: + req = Requirement(req_str) + except Exception: + continue + # Only consider requirements whose marker matches current env and extra + if req.marker is None or req.marker.evaluate(env): + selected.append(req) + + details: dict[str, dict[str, object]] = {} + all_ok = True + for req in selected: + name = req.name + spec = req.specifier + inst = dist_version(name) + if inst is None: + details[name] = {"required": str(spec), "installed": None, "ok": False, "reason": "not installed"} + all_ok = False + continue + ok = spec.contains(inst, prereleases=True) if spec else True + details[name] = { + "required": str(spec), + "installed": inst, + "ok": ok, + "reason": None if ok else "version mismatch", + } + if not ok: + all_ok = False + + return ToolStatus( + name=tool, + installed=all_ok, + details=details, + ) diff --git a/uv.lock b/uv.lock index d7c27fa..4845dd9 100644 --- a/uv.lock +++ b/uv.lock @@ -228,6 +228,8 @@ source = { editable = "." } [package.optional-dependencies] all = [ { name = "ruff" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomli-w" }, ] dev = [ { name = "pytest" }, @@ -235,6 +237,8 @@ dev = [ ] ruff = [ { name = "ruff" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomli-w" }, ] [package.metadata] @@ -243,6 +247,10 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, { name = "ruff", marker = "extra == 'all'", specifier = ">=0.6" }, { name = "ruff", marker = "extra == 'ruff'", specifier = ">=0.6" }, + { name = "tomli", marker = "python_full_version < '3.11' and extra == 'all'", specifier = ">=2.0.1" }, + { name = "tomli", marker = "python_full_version < '3.11' and extra == 'ruff'", specifier = ">=2.0.1" }, + { name = "tomli-w", marker = "extra == 'all'", specifier = ">=1.0.0" }, + { name = "tomli-w", marker = "extra == 'ruff'", specifier = ">=1.0.0" }, ] provides-extras = ["all", "dev", "ruff"] @@ -295,6 +303,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" From 6be850ce0ecbf98f145e448077568a0e6c98b351 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Sun, 2 Nov 2025 02:49:48 +0100 Subject: [PATCH 12/13] fix tool reporting --- src/score_tools/cli.py | 6 +++--- src/score_tools/utils.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/score_tools/cli.py b/src/score_tools/cli.py index 8b0c5fc..74ebb00 100644 --- a/src/score_tools/cli.py +++ b/src/score_tools/cli.py @@ -17,7 +17,7 @@ import json import shutil import sys -from dataclasses import asdict, dataclass +from dataclasses import asdict from typing import Any from . import __version__ @@ -30,7 +30,7 @@ def _is_cmd_available(cmd: str) -> bool: def _detect_tool_score_ruff() -> ToolStatus: - return is_installed("ruff") + return is_installed(tool="score-ruff", extra="ruff") def _available_package_managers() -> list[str]: @@ -88,7 +88,7 @@ def _pretty_print_status(status: dict[str, Any]) -> None: print("Embedded tools:") for name, data in status["tools"].items(): installed = "yes" if data.get("installed") else "no" - print(f" - {name:11} installed: {installed:3}") + print(f" - {name} installed: {installed:3}") print("") if status["package_managers"]: diff --git a/src/score_tools/utils.py b/src/score_tools/utils.py index 5c9e1f2..dd022de 100644 --- a/src/score_tools/utils.py +++ b/src/score_tools/utils.py @@ -62,7 +62,7 @@ class ToolStatus: details: dict[str, dict[str, object]] -def is_installed(tool: str) -> ToolStatus: +def is_installed(tool: str, extra: str) -> ToolStatus: """Generic check that all requirements for an extra are satisfied. Returns (all_ok, details) where details maps requirement names to: @@ -70,7 +70,7 @@ def is_installed(tool: str) -> ToolStatus: This avoids hard-coding dependency names by reading the installed distribution's "Requires-Dist" entries and selecting those tied to the - 'ruff' extra (respecting environment markers like python_version). + given extra (respecting environment markers like python_version). """ try: from packaging.markers import default_environment # type: ignore @@ -92,7 +92,7 @@ def is_installed(tool: str) -> ToolStatus: dist = metadata.distribution("score-tools") requires = dist.requires or [] env: dict[str, str] = {k: str(v) for k, v in default_environment().items()} - env["extra"] = str(tool) + env["extra"] = str(extra) selected: list[Requirement] = [] for req_str in requires: From f5c139e782f64c6c2acfed7f29b19bb1233b744f Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Wed, 12 Nov 2025 21:59:21 +0100 Subject: [PATCH 13/13] cleanup CI --- .github/workflows/ci.yml | 171 ++++++++++++++++++---- .github/workflows/installer-and-tests.yml | 166 --------------------- .pre-commit-config.yaml | 19 --- 3 files changed, 146 insertions(+), 210 deletions(-) delete mode 100644 .github/workflows/installer-and-tests.yml delete mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef8a9a3..8755f26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,48 +2,169 @@ name: CI on: push: - branches: [ main ] pull_request: - branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read jobs: - lint: + build: runs-on: ubuntu-latest + outputs: + sdist: ${{ steps.paths.outputs.sdist }} + wheel: ${{ steps.paths.outputs.wheel }} steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.12" - - name: Install package and deps + cache: pip + + - name: Build + run: | + # Build sdist and wheel artifacts from the checked-out source. + # Keep this job minimal and deterministic. + python -m pip install -U pip build + python -m build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/* + + - id: paths run: | - python -m pip install --upgrade pip - python -m pip install -e '.[dev,ruff]' - - name: Run ruff check - run: ruff check src/ tests/ - - name: Run ruff format check - run: ruff format --check src/ tests/ - - test: + # Record artifact paths for downstream jobs (convenience only). + echo "sdist=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" + echo "wheel=$(ls dist/*.whl | head -n1)" >> "$GITHUB_OUTPUT" + + tests: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python: ["3.10","3.11","3.12","3.13"] runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + # Caching will fail if there is nothing to cache + #cache: pip + + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist } + + - uses: astral-sh/setup-uv@v7 + + - name: Install from wheel + dev deps + run: | + # Prefer uv for speed; the tool was set up in the previous step. + # Install the built wheel so tests import the installed artifact, not the repo's src/. + uv pip install --system "${{ needs.build.outputs.wheel }}" + uv pip install --system 'pytest>=8' 'ruff>=0.6' 'mypy>=1.10' 'coverage[toml]>=7' + + - run: | + mkdir -p reports + + # Run tests against the installed wheel, not the source tree's `src`. + # `-c /dev/null` ignores repo pytest config (e.g., pythonpath=['src']). + python -m coverage run -m pytest -q -c /dev/null tests --junitxml=reports/junit.xml + + # Export coverage for upload. + python -m coverage xml -o reports/coverage.xml + + # TODO: do something with reports? run coverage only for one system? + + installer-uvx: + needs: build strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.12", "3.13"] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} - - name: Install package and test deps + python-version: "3.12" + cache: pip + + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist } + + - uses: astral-sh/setup-uv@v7 + + - name: uvx smoke run: | - python -m pip install --upgrade pip - python -m pip install -e '.[dev]' - - name: Run tests with coverage - run: pytest --cov=score_tools --cov-report=xml --cov-report=term - - name: Upload coverage to Codecov - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - uses: codecov/codecov-action@v4 + # Validate the CLI runs when installed via uvx directly from the wheel. + uvx --from "${{ needs.build.outputs.wheel }}" score-tools --help + + installer-pipx: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist } + + - name: pipx smoke + run: | + # Validate the CLI runs via pipx using an ephemeral environment from the built wheel. + # This avoids publishing requirements and extra PATH handling. + python -m pip install -U pip pipx + python -m pipx run --spec "${{ needs.build.outputs.wheel }}" score-tools --help + + installer-pip: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 with: - file: ./coverage.xml - fail_ci_if_error: false + python-version: "3.12" + cache: pip + + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist } + + - name: pip smoke + run: | + # Validate the CLI installs and runs via pip. + # On Windows, invoking the console script can be finicky; calling module main is robust. + python -m pip install -U pip + python -m pip install "${{ needs.build.outputs.wheel }}" + python -c "import score_tools.cli as c; import sys; sys.exit(c.main([]))" + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: ruff check src/ tests/ + - run: ruff format --check src/ tests/ + - run: mypy src diff --git a/.github/workflows/installer-and-tests.yml b/.github/workflows/installer-and-tests.yml deleted file mode 100644 index 7da2b11..0000000 --- a/.github/workflows/installer-and-tests.yml +++ /dev/null @@ -1,166 +0,0 @@ -name: CI - -on: - push: - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - outputs: - sdist: ${{ steps.paths.outputs.sdist }} - wheel: ${{ steps.paths.outputs.wheel }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: pip - - - name: Build - run: | - # Build sdist and wheel artifacts from the checked-out source. - # Keep this job minimal and deterministic. - python -m pip install -U pip build - python -m build - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/* - - - id: paths - run: | - # Record artifact paths for downstream jobs (convenience only). - echo "sdist=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" - echo "wheel=$(ls dist/*.whl | head -n1)" >> "$GITHUB_OUTPUT" - - tests: - needs: build - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python: ["3.10","3.11","3.12","3.13"] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - # Caching will fail if there is nothing to cache - #cache: pip - - - uses: actions/download-artifact@v4 - with: { name: dist, path: dist } - - - uses: astral-sh/setup-uv@v7 - - - name: Install from wheel + dev deps - run: | - # Prefer uv for speed; the tool was set up in the previous step. - # Install the built wheel so tests import the installed artifact, not the repo's src/. - uv pip install --system "${{ needs.build.outputs.wheel }}" - uv pip install --system 'pytest>=8' 'ruff>=0.6' 'mypy>=1.10' 'coverage[toml]>=7' - - - run: python -m ruff check src tests - - - run: python -m mypy src - - - run: | - mkdir -p reports - - # Run tests against the installed wheel, not the source tree's `src`. - # `-c /dev/null` ignores repo pytest config (e.g., pythonpath=['src']). - python -m coverage run -m pytest -q -c /dev/null tests --junitxml=reports/junit.xml - - # Export coverage for upload. - python -m coverage xml -o reports/coverage.xml - - # TODO: do something with reports? run coverage only for one system? - - installer-uvx: - needs: build - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: pip - - - uses: actions/download-artifact@v4 - with: { name: dist, path: dist } - - - uses: astral-sh/setup-uv@v7 - - - name: uvx smoke - run: | - # Validate the CLI runs when installed via uvx directly from the wheel. - uvx --from "${{ needs.build.outputs.wheel }}" score-tools --help - - installer-pipx: - needs: build - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: pip - - - uses: actions/download-artifact@v4 - with: { name: dist, path: dist } - - - name: pipx smoke - run: | - # Validate the CLI runs via pipx using an ephemeral environment from the built wheel. - # This avoids publishing requirements and extra PATH handling. - python -m pip install -U pip pipx - python -m pipx run --spec "${{ needs.build.outputs.wheel }}" score-tools --help - - installer-pip: - needs: build - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: pip - - - uses: actions/download-artifact@v4 - with: { name: dist, path: dist } - - - name: pip smoke - run: | - # Validate the CLI installs and runs via pip. - # On Windows, invoking the console script can be finicky; calling module main is robust. - python -m pip install -U pip - python -m pip install "${{ needs.build.outputs.wheel }}" - python -c "import score_tools.cli as c; import sys; sys.exit(c.main([]))" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index a75ecf7..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# See https://pre-commit.com for more information -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-toml - - id: check-added-large-files - - id: check-merge-conflict - - id: mixed-line-ending - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.4 - hooks: - - id: ruff - args: [--fix] - - id: ruff-format