Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Python CI

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.9.6"

- name: Set up Python
run: uv python install

- name: Install dependencies
run: make update

- name: Run tests
run: make test

- name: Lint check
run: make lint-check
107 changes: 107 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Default target
.DEFAULT_GOAL := help

# Colors for output
BLUE := \033[36m
GREEN := \033[32m
YELLOW := \033[33m
RED := \033[31m
RESET := \033[0m

# Configuration
VENV_NAME := .venv
TEST_PATH := tests/
SRC_PATH := src/

# Command shortcuts
UV := uv
RUFF := uv run ruff
TY := uv run ty
PYTEST := uv run pytest


.PHONY: help
help: ## Show this help message
@echo "$(BLUE)MDVerse Grodecoder$(RESET)"
@echo "Available targets:"
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(GREEN)%-20s$(RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST)

# Environment setup
.PHONY: venv
venv: ## Create a virtual environment using uv
@echo "$(BLUE)Creating virtual environment...$(RESET)"
$(UV) sync
@echo "$(GREEN)Virtual environment created. Activate with: source $(VENV_NAME)/bin/activate$(RESET)"

.PHONY: install
install: ## Install package dependencies
@echo "$(BLUE)Installing dependencies...$(RESET)"
$(UV) pip install -e .
@echo "$(GREEN)Dependencies installed$(RESET)"

.PHONY: install-dev
install-dev: ## Install package with development dependencies
@echo "$(BLUE)Installing development dependencies...$(RESET)"
$(UV) pip install -e ".[test,dev]"
@echo "$(GREEN)Development dependencies installed$(RESET)"

.PHONY: deps
deps: ## Update and sync dependencies
@echo "$(BLUE)Updating dependencies...$(RESET)"
$(UV) lock
$(UV) sync
@echo "$(GREEN)Dependencies updated$(RESET)"

# Package management
update: ## Update all dependencies to latest versions
@echo "$(BLUE)Updating dependencies...$(RESET)"
$(UV) lock --upgrade
$(UV) sync
@echo "$(GREEN)Dependencies updated$(RESET)"

.PHONY: build
build: clean ## Build the package
@echo "$(BLUE)Building package...$(RESET)"
$(UV) build
@echo "$(GREEN)Package built successfully$(RESET)"

# Cleaning tasks
.PHONY: clean
clean: ## Clean build artifacts
@echo "$(BLUE)Cleaning build artifacts...$(RESET)"
rm -rf build/
rm -rf dist/
rm -rf *.egg-info/
rm -rf .pytest_cache/
rm -rf .mypy_cache/
rm -rf .ruff_cache/
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
find . -type f -name "*.pyo" -delete
@echo "$(GREEN)Build artifacts cleaned$(RESET)"

# Testing tasks
.PHONY: test
test: ## Run tests with pytest
@echo "$(BLUE)Running tests...$(RESET)"
$(PYTEST) $(TEST_PATH)
@echo "$(GREEN)Tests completed$(RESET)"

.PHONY: test-verbose
test-verbose: ## Run tests with verbose output
@echo "$(BLUE)Running tests with verbose output...$(RESET)"
$(PYTEST) $(TEST_PATH) -v -s --tb=long
@echo "$(GREEN)Tests completed$(RESET)"

# Code quality tasks
lint-check: ## Check if code is properly formatted
@echo "$(BLUE)Running linter...$(RESET)"
$(RUFF) check $(SRC_PATH) $(TEST_PATH)
$(TY) check $(SRC_PATH)
@echo "$(GREEN)Linting completed$(RESET)"

lint-format: ## Format code with ruff
@echo "$(BLUE)Formatting code...$(RESET)"
$(RUFF) format $(SRC_PATH) $(TEST_PATH)
$(RUFF) check $(SRC_PATH) $(TEST_PATH) --fix
@echo "$(GREEN)Code formatted$(RESET)"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ grodecoder = "grodecoder.cli:cli"
[dependency-groups]
dev = [
"icecream>=2.1.4",
"mypy>=1.17.1",
"pytest>=8.3.5",
"ruff>=0.11.2",
"types-requests>=2.32.4.20250809",
Expand All @@ -32,6 +31,7 @@ dev = [
"requests>=2.32.3",
"streamlit>=1.44.0",
"watchdog>=6.0.0",
"ty==0.0.1a26",
]

[tool.ruff]
Expand Down
1 change: 1 addition & 0 deletions src/grodecoder/identifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def identify_small_molecule(

def identify(universe: UniverseLike, bond_threshold: float = 5.0) -> Inventory:
"""Identifies the molecules in a structure file."""
assert universe.atoms is not None # avoids ty error "Expect Sized"
timer_start = time.perf_counter() # do not include structure reading time in the performance measurement
inventory = _identify(universe, bond_threshold)
elapsed = time.perf_counter() - timer_start
Expand Down
17 changes: 11 additions & 6 deletions src/grodecoder/toputils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ def formula(molecule: UniverseLike, elements: Iterable[str] = ("C", "H", "O", "N

The formula is based on the first residue alone.
"""
atoms = molecule.atoms.residues[0].atoms
residues = getattr(molecule.atoms, "residues", [])
assert len(residues) > 0
atoms = residues[0].atoms
count = collections.Counter(_first_alpha(atom.name) for atom in atoms)
formula = "".join(
f"{element}{count[element]}" if count[element] > 1 else element
Expand All @@ -37,7 +39,7 @@ def sequence(atoms: UniverseLike) -> str:
"""Returns the protein sequence from a set of atoms."""
residue_names = get_amino_acid_name_map()
residue_names.update(get_nucleotide_name_map())
return "".join(residue_names.get(residue.resname, "X") for residue in atoms.residues)
return "".join(residue_names.get(residue.resname, "X") for residue in getattr(atoms, "residues", []))


def has_bonds(residue: Residue, cutoff: float = 2.0):
Expand Down Expand Up @@ -102,13 +104,15 @@ def end_of_chain():

segments = []

assert (residues := getattr(universe, "residues", [])) and len(residues) > 0

start_current_chain = 0
for i, current_residue in enumerate(universe.residues[:-1]):
next_residue = universe.residues[i + 1]
for i, current_residue in enumerate(residues[:-1]):
next_residue = residues[i + 1]
if end_of_chain():
segments.append((start_current_chain, i))
start_current_chain = i + 1
segments.append((start_current_chain, len(universe.residues) - 1))
segments.append((start_current_chain, len(residues) - 1))

return segments

Expand All @@ -123,7 +127,8 @@ def guess_resolution(universe: UniverseLike) -> MolecularResolution:
If none of the first five residues have bonds, the resolution is considered coarse-grained.
"""
# Select the first five residues with at least two atoms.
residues = [residue for residue in universe.residues if len(residue.atoms) >= 2][:5]
assert (residues := getattr(universe, "residues", [])) and len(residues) > 0
residues = [residue for residue in residues if len(residue.atoms) >= 2][:5]
for residue in residues:
if has_bonds(residue, cutoff=2.0):
return MolecularResolution.ALL_ATOM
Expand Down
Loading