diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 00e4f41..c204aaf 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -7,72 +7,72 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Set up Python + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + - name: Install dependencies - run: | - python3 -m pip install pipenv - pipenv install --dev + run: uv sync lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: 3.11 + python-version: 3.13 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Install dependencies - run: python3 -m pip install ".[dev]" + run: uv sync --extra dev - name: Check style - run: python3 -m ruff check . && python3 -m ruff format --check . + run: uv run ruff check && uv run ruff format --check + + precommit_hooks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: j178/prek-action@v1 + with: + extra_args: '--all-files --skip "ruff-format" --skip "ruff-check"' docs: runs-on: ubuntu-latest env: SPHINX_GITHUB_CHANGELOG_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: 3.11 + python-version: 3.12 # broken on 3.13 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Install dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install '.[docs]' + run: uv sync --extra docs - name: Attempt docs build working-directory: ./docs - run: make html - - precommit_hooks: - runs-on: ubuntu-latest - strategy: - matrix: - cmd: - - "end-of-file-fixer" - - "trailing-whitespace" - - "mixed-line-ending" - steps: - - uses: actions/checkout@v4 - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - - uses: pre-commit/action@v3.0.1 - with: - extra_args: ${{ matrix.cmd }} --all-files + run: source ../.venv/bin/activate && make html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e34bc3b..6fcb5f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,28 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 # pre-commit-hooks version + rev: v6.0.0 # pre-commit-hooks version hooks: - - id: check-added-large-files - - id: detect-private-key - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-merge-conflict - id: detect-aws-credentials args: [ --allow-missing-credentials ] + - repo: builtin + hooks: + - id: trailing-whitespace + - id: check-added-large-files + args: ['--maxkb=1500'] + - id: check-case-conflict + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: check-json + - id: check-toml + - id: check-yaml - id: mixed-line-ending args: [ --fix=lf ] + - id: check-merge-conflict + - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.1 + rev: v0.14.10 # ruff version hooks: - id: ruff-format - - id: ruff + - id: ruff-check args: [ --fix, --exit-non-zero-on-fix ] -minimum_pre_commit_version: 4.2.0 +minimum_prek_version: 0.2.23 diff --git a/pyproject.toml b/pyproject.toml index 0499cd4..c0ec7ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ classifiers = [ "Intended Audience :: Science/Research", "Intended Audience :: Developers", "Topic :: Scientific/Engineering :: Bio-Informatics", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -22,7 +21,8 @@ classifiers = [ ] requires-python = ">=3.11" description = "Common Operation on Lots of Sequences Tool" -license = {file = "LICENSE"} +license = "MIT" +license-files = ["LICENSE"] dependencies = [ "asyncpg", "boto3", @@ -38,11 +38,11 @@ dynamic = ["version"] [project.optional-dependencies] dev = [ - "pre-commit>=4.2.0", + "prek>=0.2.23", "ipython", "ipykernel", "psycopg2-binary", - "ruff==0.12.1", + "ruff==0.14.10", ] tests = ["pytest", "pytest-cov", "pytest-asyncio", "mock"] docs = [ @@ -68,13 +68,6 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] -# Scanning for namespace packages in the ``src`` directory is true by -# default in pyproject.toml, so you do NOT need to include the -# `tool.setuptools.packages.find` if it looks like the following: -# [tool.setuptools.packages.find] -# namespaces = true -# where = ["src"] - [tool.setuptools.package-data] "cool_seq_tool.resources" = ["transcript_mapping.tsv"] @@ -91,113 +84,78 @@ branch = true [tool.ruff] src = ["src"] exclude = ["docs/source/conf.py"] -lint.select = [ - "F", # https://docs.astral.sh/ruff/rules/#pyflakes-f - "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w - "I", # https://docs.astral.sh/ruff/rules/#isort-i - "N", # https://docs.astral.sh/ruff/rules/#pep8-naming-n - "D", # https://docs.astral.sh/ruff/rules/#pydocstyle-d - "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up - "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann - "ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async - "S", # https://docs.astral.sh/ruff/rules/#flake8-bandit-s - "B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b - "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a - "C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 - "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz - "T10", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz - "EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em - "LOG", # https://docs.astral.sh/ruff/rules/#flake8-logging-log - "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g - "INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp - "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie - "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - "Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q - "RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse - "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret - "SLF", # https://docs.astral.sh/ruff/rules/#flake8-self-slf - "SLOT", # https://docs.astral.sh/ruff/rules/#flake8-slots-slot - "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim - "ARG", # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg - "PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth - "PGH", # https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh - "PLC", # https://docs.astral.sh/ruff/rules/#convention-c - "PLE", # https://docs.astral.sh/ruff/rules/#error-e_1 - "TRY", # https://docs.astral.sh/ruff/rules/#tryceratops-try - "PERF", # https://docs.astral.sh/ruff/rules/#perflint-perf - "FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb - "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf -] -lint.fixable = [ - "I", - "F401", - "D", - "UP", - "ANN", - "B", - "C4", - "LOG", - "G", - "PIE", - "PT", - "RSE", - "SIM", - "PLC", - "PLE", - "TRY", - "PERF", - "FURB", - "RUF" -] -# ANN003 - missing-type-kwargs -# D203 - one-blank-line-before-class -# D205 - blank-line-after-summary -# D206 - indent-with-spaces* -# D213 - multi-line-summary-second-line -# D300 - triple-single-quotes* -# D400 - ends-in-period -# D415 - ends-in-punctuation -# E111 - indentation-with-invalid-multiple* -# E114 - indentation-with-invalid-multiple-comment* -# E117 - over-indented* -# E501 - line-too-long* -# W191 - tab-indentation* -# S321 - suspicious-ftp-lib-usage -# PLC0206 - dict-index-missing-items -# *ignored for compatibility with formatter -lint.ignore = [ - "ANN003", - "D203", "D205", "D206", "D213", "D300", "D400", "D415", - "E111", "E114", "E117", "E501", +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + # unused + "AIR", + "ERA", + "YTT", + "BLE", + "FBT", + "CPY", + "DJ", + "EXE", + "FIX", + "FA", + "PYI", + "TD", + "C90", + "NPY", + "PD", + # ignore for compatibility with formatter + "D206", + "D300", + "W191", + "E111", + "E114", + "E117", + "E501", "W191", "S321", + "COM812", + "COM819", + "Q000", + "Q001", + "Q002", + "Q003", + # don't require types on *args, **kwargs + "ANN002", + "ANN003", + # subjective pylint thresholds + "PLR0904", + "PLR091", + "PLR1702", "PLC0206", + # misc unnecessary stuff + "S321", + "D203", + "D205", + "D213", + "D400", + "D415", ] [tool.ruff.lint.per-file-ignores] # ANN001 - missing-type-function-argument # ANN2 - missing-return-type -# D100 - undocumented-public-module -# D102 - undocumented-public-class -# D103 - undocumented-public-function # N805 - invalid-first-argument-name-for-method # F821 - undefined-name # F401 - unused-import # INP001 - implicit-namespace-package # SLF001 - private-member-access # ARG001 - unused-function-argument +# PLR2004 - magic-value-comparison "tests/*" = [ "ANN001", "ANN2", - "D100", - "D102", - "D103", + "D", "S101", "INP001", "SLF001", - "ARG001" + "ARG001", + "PLR2004", ] "*__init__.py" = ["F401"] "src/cool_seq_tool/schemas.py" = ["ANN201", "N805", "ANN001"] diff --git a/src/cool_seq_tool/handlers/seqrepo_access.py b/src/cool_seq_tool/handlers/seqrepo_access.py index 76a8fb0..035c530 100644 --- a/src/cool_seq_tool/handlers/seqrepo_access.py +++ b/src/cool_seq_tool/handlers/seqrepo_access.py @@ -58,9 +58,8 @@ def get_reference_sequence( start, end = get_inter_residue_pos(start, end, coordinate_type) if start == end: end += 1 - else: - if start is not None and coordinate_type == CoordinateType.RESIDUE: - start -= 1 + elif start is not None and coordinate_type == CoordinateType.RESIDUE: + start -= 1 try: sequence = self.sr.fetch(ac, start=start, end=end) diff --git a/src/cool_seq_tool/mappers/alignment.py b/src/cool_seq_tool/mappers/alignment.py index aaf3514..70c7432 100644 --- a/src/cool_seq_tool/mappers/alignment.py +++ b/src/cool_seq_tool/mappers/alignment.py @@ -68,11 +68,10 @@ async def p_to_c( # codon. We want to return inter-residue (0-based), so we subtract 1 from this. if coordinate_type == CoordinateType.RESIDUE: c_pos = (p_start_pos * 3) - 3, p_end_pos * 3 + elif p_start_pos == p_end_pos: + c_pos = ((p_start_pos + 1) * 3) - 3, (p_end_pos + 1) * 3 else: - if p_start_pos == p_end_pos: - c_pos = ((p_start_pos + 1) * 3) - 3, (p_end_pos + 1) * 3 - else: - c_pos = ((p_start_pos + 1) * 3) - 3, p_end_pos * 3 + c_pos = ((p_start_pos + 1) * 3) - 3, p_end_pos * 3 return { "c_ac": c_ac, diff --git a/src/cool_seq_tool/mappers/exon_genomic_coords.py b/src/cool_seq_tool/mappers/exon_genomic_coords.py index f036c08..f206040 100644 --- a/src/cool_seq_tool/mappers/exon_genomic_coords.py +++ b/src/cool_seq_tool/mappers/exon_genomic_coords.py @@ -723,11 +723,10 @@ def _get_tx_segment( seg_genomic_pos = offset + genomic_ac_data.alt_start_i else: seg_genomic_pos = genomic_ac_data.alt_end_i - offset + elif strand == Strand.POSITIVE: + seg_genomic_pos = offset + genomic_ac_data.alt_end_i else: - if strand == Strand.POSITIVE: - seg_genomic_pos = offset + genomic_ac_data.alt_end_i - else: - seg_genomic_pos = genomic_ac_data.alt_start_i - offset + seg_genomic_pos = genomic_ac_data.alt_start_i - offset genomic_loc, err_msg = self._get_vrs_seq_loc( genomic_ac, diff --git a/src/cool_seq_tool/mappers/feature_overlap.py b/src/cool_seq_tool/mappers/feature_overlap.py index 1fcf64a..5915679 100644 --- a/src/cool_seq_tool/mappers/feature_overlap.py +++ b/src/cool_seq_tool/mappers/feature_overlap.py @@ -158,14 +158,13 @@ def get_grch38_mane_gene_cds_overlap( if not re.match(f"^{CHR_PATTERN}$", chromosome): error_msg = "`chromosome` must be 1, ..., 22, X, or Y" raise FeatureOverlapError(error_msg) + elif identifier: + chromosome = self._get_chr_from_alt_ac(identifier) + if identifier.startswith("ga4gh:SQ."): + ga4gh_seq_id = identifier else: - if identifier: - chromosome = self._get_chr_from_alt_ac(identifier) - if identifier.startswith("ga4gh:SQ."): - ga4gh_seq_id = identifier - else: - error_msg = "Must provide either `chromosome` or `identifier`" - raise FeatureOverlapError(error_msg) + error_msg = "Must provide either `chromosome` or `identifier`" + raise FeatureOverlapError(error_msg) # Convert residue to inter-residue if coordinate_type == CoordinateType.RESIDUE: @@ -234,8 +233,8 @@ def _get_seq_loc( resp = {} refget_ac = ga4gh_seq_id.split("ga4gh:")[-1] - for gene, group in feature_df.group_by("gene"): - gene = gene[0] + for gene_l, group in feature_df.group_by("gene"): + gene = gene_l[0] _gene_overlap_data = [ CdsOverlap( cds=_get_seq_loc( diff --git a/src/cool_seq_tool/mappers/mane_transcript.py b/src/cool_seq_tool/mappers/mane_transcript.py index 94c2d76..b02c2cb 100644 --- a/src/cool_seq_tool/mappers/mane_transcript.py +++ b/src/cool_seq_tool/mappers/mane_transcript.py @@ -545,10 +545,9 @@ def _validate_reading_frames( new_rf, ) return False - else: - if pos_index == 0: - _logger.warning("%s must having start position", ac) - return False + elif pos_index == 0: + _logger.warning("%s must having start position", ac) + return False return True def _validate_references( diff --git a/src/cool_seq_tool/sources/uta_database.py b/src/cool_seq_tool/sources/uta_database.py index 2f15fb4..87651f2 100644 --- a/src/cool_seq_tool/sources/uta_database.py +++ b/src/cool_seq_tool/sources/uta_database.py @@ -664,11 +664,10 @@ async def get_genomic_tx_data( genomic_tx_data.alt_pos_range[0] + pos_change[0], genomic_tx_data.alt_pos_range[1] - pos_change[1], ) + elif genomic_tx_data.strand == Strand.NEGATIVE: + alt_pos_change_range = (pos[1], pos[0]) else: - if genomic_tx_data.strand == Strand.NEGATIVE: - alt_pos_change_range = (pos[1], pos[0]) - else: - alt_pos_change_range = pos + alt_pos_change_range = pos return GenomicTxMetadata( **genomic_tx_data.model_dump(), @@ -953,7 +952,7 @@ def database(self) -> str | None: def schema(self) -> str | None: """Create schema property.""" path_elems = self.path.split("/") - return path_elems[2] if len(path_elems) > 2 else None + return path_elems[2] if len(path_elems) > 2 else None # noqa: PLR2004 @property def sanitized_url(self) -> str: