Linting and CI
==============
Code quality tools
------------------
Ruff
^^^^
`Ruff `_ is the linter and formatter. It is
configured in ``pyproject.toml``:
.. code-block:: toml
[tool.ruff]
target-version = "py312"
line-length = 80
src = ["src"]
[tool.ruff.lint]
select = [
"B", # flake8-bugbear
"E", # pycodestyle errors
"F", # pyflakes
"N", # pep8-naming
"PTH", # flake8-use-pathlib
"RUF", # Ruff-specific rules
"S", # flake8-bandit (security)
"SIM", # flake8-simplify
"UP", # pyupgrade
"W", # pycodestyle warnings
]
The rule set was chosen to catch common bugs (``B``), security issues (``S``),
outdated patterns (``UP``) and unnecessary complexity (``SIM``) without being
overly noisy. Per-file ignores are applied where rules conflict with intentional
patterns:
- ``lang.py`` - Cyrillic characters trigger ``RUF001``.
- ``pdfopt.py`` - broad ``except: pass`` is intentional for best-effort
optimization (``S110``, ``S112``).
- ``providers/*`` - ``urlopen`` calls are expected (``S310``).
- ``tests/*`` - ``assert`` is the standard way to write checks (``S101``).
- ``test_pdfopt.py`` - late imports after ``pytest.importorskip`` (``E402``).
Run ruff manually::
ruff check .
Mypy
^^^^
`Mypy `_ runs in strict mode:
.. code-block:: toml
[tool.mypy]
python_version = "3.12"
strict = true
Two overrides relax strictness where full typing is impractical:
- ``livereload.*`` - no type stubs available (``ignore_missing_imports``).
- ``tests.*`` - fixtures and mocks are hard to type fully
(``disallow_untyped_defs = false``).
Run mypy manually::
mypy src/
Pre-commit hooks
----------------
The ``.pre-commit-config.yaml`` runs three hook sets before every commit:
1. **pre-commit-hooks** (v6.0.0) - trailing whitespace, end-of-file fixer,
YAML check.
2. **ruff** (v0.15.5) - linting.
3. **mypy** (v1.19.0) - type checking with ``types-tqdm`` stubs.
Install the hooks (one-time setup)::
pre-commit install
Run all hooks against every file manually::
pre-commit run --all-files
CI pipelines
------------
Three GitHub Actions workflows automate quality checks and artifact generation.
test.yml
^^^^^^^^
Triggered on every push to ``main`` and every pull request.
**lint** job:
1. Install ``pip install -e .[dev]``
2. ``ruff check .``
3. ``mypy src/``
4. Generate and upload a lint badge.
**test** job:
1. Install ``pip install -e .[dev]``
2. ``pytest --cov --cov-report=term --cov-report=xml:coverage.xml``
3. Generate and upload a coverage badge.
**publish** job (main branch only):
Pushes the lint and coverage badge SVGs to the ``_artifacts`` orphan branch so
they are available at stable raw URLs for the README badges.
build-pdf.yml
^^^^^^^^^^^^^
Triggered on every push to ``main`` and every pull request. Builds PDFs for
multiple countries using a matrix strategy (``pl``, ``by``, ``us``, ``kr``). On
the main branch, the resulting PDFs are pushed to the ``_artifacts`` branch.
build-sphinx-docs.yml
^^^^^^^^^^^^^^^^^^^^^
Triggered on push to ``main``. Builds the Sphinx HTML documentation and deploys
it to GitHub Pages.
The ``_artifacts`` branch
-------------------------
Instead of GitHub Releases with moving tags, badge SVGs and pre-built PDFs are
pushed to an orphan branch called ``_artifacts``. This gives stable raw URLs
like::
https://github.com/stamerlan/feather-flow/raw/_artifacts/coverage-badge.svg
The release-based approach had problems:
- Moving tags (e.g. ``latest``) cause confusion with ``git fetch``.
- Release assets require authentication for private repos.
- The release page becomes cluttered with auto-generated entries.
The ``_artifacts`` branch provides stable raw URLs, works without
authentication, and keeps the release history clean for actual version releases.