diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index aa12ffb..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" - target-branch: "develop" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "python" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28733b1..fbb6380 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.12'] + python-version: ['3.9', '3.12'] steps: - uses: actions/checkout@v4 - name: Set up PDM @@ -26,6 +26,9 @@ jobs: - name: Run linter run: | pdm run ruff check . + - name: Run type checker + run: | + pdm run mypy xyz2graph - name: Run tests run: | pdm run pytest --cov --cov-report=xml --junitxml=junit.xml -o junit_family=legacy diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d0dee0c..94cd52a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: - name: Run linter run: python -m ruff check . - name: Run type checker - run: python -m mypy . + run: python -m mypy xyz2graph - name: Build package run: | python -m pip install hatchling hatch-vcs @@ -44,7 +44,7 @@ jobs: - name: Create Release uses: softprops/action-gh-release@v2 with: - body_path: CHANGELOG.md + generate_release_notes: true - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: diff --git a/README.md b/README.md index 42311ff..444dcb7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/zotko/xyz2graph/ci.yml)](https://github.com/zotko/xyz2graph/actions) [![codecov](https://codecov.io/gh/zotko/xyz2graph/branch/develop/graph/badge.svg?token=TXP63VKUIP)](https://codecov.io/gh/zotko/xyz2graph) -[![Dependabot](https://img.shields.io/badge/dependabot-enabled-025E8C?logo=dependabot&logoColor=white)](https://github.com/zotko/xyz2graph/network/updates) [![PyPI Downloads](https://static.pepy.tech/badge/xyz2graph/month)](https://pepy.tech/projects/xyz2graph) [![GitHub Stars](https://img.shields.io/github/stars/zotko/xyz2graph)](https://github.com/zotko/xyz2graph/stargazers) @@ -33,7 +32,7 @@ pip install xyz2graph ## Requirements -- Python 3.8+ +- Python 3.9+ - Dependencies: NumPy, Plotly, NetworkX ## Quick Start diff --git a/pyproject.toml b/pyproject.toml index d8e7317..5617a67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,28 +41,10 @@ Repository = "https://github.com/zotko/xyz2graph" Documentation = "https://zotko.github.io/xyz2graph" "Bug Tracker" = "https://github.com/zotko/xyz2graph/issues" -[tool.pdm.dev-dependencies] -docs = [ - "mkdocs>=1.6.0", - "mkdocs-material>=9.5.0", - "mkdocstrings-python>=1.12.0", - "mkdocs-include-markdown-plugin>=7.1.5" -] -dev = [ - "pytest>=7.0.0,<8.0.0", - "pytest-cov>=4.0.0,<5.0.0", - "ruff>=0.1.0,<1.0.0", - "mypy>=1.0.0,<2.0.0", - "build>=1.0.0,<2.0.0", - "twine>=4.0.0,<5.0.0", - "pre-commit>=4.1.0", -] - [tool.ruff] -target-version = "py38" +target-version = "py39" line-length = 100 src = ["xyz2graph"] -include = ["test/test_*.py"] [tool.ruff.lint.per-file-ignores] "{__init__.py,tests/*,helpers.py,logging.py}" = ["D"] @@ -79,12 +61,17 @@ lines-after-imports = 2 convention = "google" [tool.mypy] +files = ["xyz2graph"] python_version = "3.9" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true check_untyped_defs = true +[[tool.mypy.overrides]] +module = ["plotly.*"] +ignore_missing_imports = true + [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] @@ -92,3 +79,21 @@ addopts = "--cov=xyz2graph --cov-report=term-missing" [project.scripts] xyz2graph = "xyz2graph.cli:main" + +[dependency-groups] +dev = [ + "pytest>=7.0.0,<8.0.0", + "pytest-cov>=4.0.0,<5.0.0", + "ruff>=0.1.0,<1.0.0", + "mypy>=1.0.0,<2.0.0", + "types-networkx>=3.4.2.20250509", + "build>=1.0.0,<2.0.0", + "twine>=4.0.0,<5.0.0", + "pre-commit>=4.1.0", +] +docs = [ + "mkdocs>=1.6.0", + "mkdocs-material>=9.5.0", + "mkdocstrings-python>=1.12.0", + "mkdocs-include-markdown-plugin>=7.1.5", +] diff --git a/tests/test_molgraph.py b/tests/test_molgraph.py index 91269a0..aa4f111 100644 --- a/tests/test_molgraph.py +++ b/tests/test_molgraph.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import List import numpy as np import pytest @@ -181,7 +180,7 @@ def test_property_consistency() -> None: (["Fe", "C", "O", "O", "O", "O", "O"], "CFeO5"), # Complex case ], ) -def test_formula(elements: List[str], expected_formula: str) -> None: +def test_formula(elements: list[str], expected_formula: str) -> None: """Test molecular formula generation.""" mol = MolGraph() for i, el in enumerate(elements): diff --git a/xyz2graph/cli.py b/xyz2graph/cli.py index cbee39e..5a7ac65 100644 --- a/xyz2graph/cli.py +++ b/xyz2graph/cli.py @@ -6,7 +6,7 @@ import sys import tempfile from pathlib import Path -from typing import List, Tuple, Union +from typing import Union import plotly.offline as offline from plotly.io import write_html @@ -15,7 +15,7 @@ from .logging import logger -def parse_remove_arg(remove_str: str) -> Tuple[List[int], List[str]]: +def parse_remove_arg(remove_str: str) -> tuple[list[int], list[str]]: """Parse remove argument string into indices and elements. Args: @@ -48,7 +48,7 @@ def parse_remove_arg(remove_str: str) -> Tuple[List[int], List[str]]: return indices, elements -def format_remove_message(indices: List[int], elements: List[str]) -> str: +def format_remove_message(indices: list[int], elements: list[str]) -> str: """Format the message showing which atoms will be removed. Args: diff --git a/xyz2graph/constants.py b/xyz2graph/constants.py index cca6f6a..3892084 100644 --- a/xyz2graph/constants.py +++ b/xyz2graph/constants.py @@ -7,10 +7,7 @@ CPK colors. """ -from typing import Dict - - -_DEFAULT_RADII: Dict[str, float] = { +_DEFAULT_RADII: dict[str, float] = { "Ac": 1.88, "Ag": 1.59, "Al": 1.35, @@ -100,7 +97,7 @@ "Zn": 1.45, "Zr": 1.56, } -_DEFAULT_CPK_COLORS: Dict[str, str] = { +_DEFAULT_CPK_COLORS: dict[str, str] = { "Ar": "cyan", "B": "salmon", "Ba": "darkgreen", diff --git a/xyz2graph/graph.py b/xyz2graph/graph.py index 94384c6..88e06b5 100755 --- a/xyz2graph/graph.py +++ b/xyz2graph/graph.py @@ -10,11 +10,7 @@ from itertools import compress from pathlib import Path from typing import ( - Dict, - FrozenSet, - List, Optional, - Set, Union, ) @@ -64,17 +60,17 @@ class MolGraph: adj_matrix (NDArray[np.int_]): Square matrix representing molecular connectivity. """ - atoms: List[Atom] = field(default_factory=list) - bonds: List[Bond] = field(default_factory=list) + atoms: list[Atom] = field(default_factory=list) + bonds: list[Bond] = field(default_factory=list) comment: str = field(default="") # Customizable parameters with defaults - default_radii: Dict[str, float] = field(default_factory=lambda: dict(_DEFAULT_RADII)) - cpk_colors: Dict[str, str] = field(default_factory=lambda: dict(_DEFAULT_CPK_COLORS)) + default_radii: dict[str, float] = field(default_factory=lambda: dict(_DEFAULT_RADII)) + cpk_colors: dict[str, str] = field(default_factory=lambda: dict(_DEFAULT_CPK_COLORS)) cpk_color_rest: str = field(default="pink") @property - def elements(self) -> List[str]: + def elements(self) -> list[str]: """Get list of elements in the molecule. Returns: @@ -83,7 +79,7 @@ def elements(self) -> List[str]: return [atom.element for atom in self.atoms] @property - def indices(self) -> List[int]: + def indices(self) -> list[int]: """Get list of atom indices. Returns: @@ -92,7 +88,7 @@ def indices(self) -> List[int]: return [atom.index for atom in self.atoms] @property - def x(self) -> List[float]: + def x(self) -> list[float]: """Get x coordinates of all atoms. Returns: @@ -101,7 +97,7 @@ def x(self) -> List[float]: return [atom.x for atom in self.atoms] @property - def y(self) -> List[float]: + def y(self) -> list[float]: """Get y coordinates of all atoms. Returns: @@ -110,7 +106,7 @@ def y(self) -> List[float]: return [atom.y for atom in self.atoms] @property - def z(self) -> List[float]: + def z(self) -> list[float]: """Get z coordinates of all atoms. Returns: @@ -128,7 +124,7 @@ def xyz(self) -> NDArray[np.float64]: return np.array([[atom.x, atom.y, atom.z] for atom in self.atoms]) @property - def atomic_radii(self) -> List[float]: + def atomic_radii(self) -> list[float]: """Get atomic radii for all atoms. Returns: @@ -137,7 +133,7 @@ def atomic_radii(self) -> List[float]: return [atom.radius for atom in self.atoms] @property - def bond_lengths(self) -> Dict[FrozenSet[int], float]: + def bond_lengths(self) -> dict[frozenset[int], float]: """Get dictionary of bond lengths. Returns: @@ -146,13 +142,13 @@ def bond_lengths(self) -> Dict[FrozenSet[int], float]: return {bond.indices: bond.length for bond in self.bonds} @property - def adj_list(self) -> Dict[int, Set[int]]: + def adj_list(self) -> dict[int, set[int]]: """Get adjacency list representation of molecular graph. Returns: Dict[int, Set[int]]: Dictionary mapping atom indices to sets of bonded atom indices. """ - adj: Dict[int, Set[int]] = {} + adj: dict[int, set[int]] = {} for bond in self.bonds: i1, i2 = tuple(bond.indices) adj.setdefault(i1, set()).add(i2) @@ -168,7 +164,7 @@ def adj_matrix(self) -> NDArray[np.int_]: are bonded and 0 otherwise. """ n = len(self.atoms) - matrix = np.zeros((n, n), dtype=np.int_) + matrix: NDArray[np.int_] = np.zeros((n, n), dtype=np.int_) for bond in self.bonds: i, j = tuple(bond.indices) matrix[i, j] = matrix[j, i] = 1 @@ -230,7 +226,7 @@ def from_string(self, xyz_string: str) -> None: ) except (IndexError, ValueError) as err: raise ValueError( - f"Invalid format in line {i+3}, expected: element x y z" + f"Invalid format in line {i + 3}, expected: element x y z" ) from err if len(self.atoms) != n_atoms: @@ -326,12 +322,13 @@ def distance_matrix(self) -> NDArray[np.float64]: """ try: distances = self.xyz[:, np.newaxis, :] - self.xyz - return np.sqrt(np.einsum("ijk,ijk->ij", distances, distances)) + result: NDArray[np.float64] = np.sqrt(np.einsum("ijk,ijk->ij", distances, distances)) + return result except MemoryError: # Fall back to loop-based method n_atoms = len(self.atoms) logger.info("Using memory-efficient method for distance calculation") - distance_matrix = np.zeros((n_atoms, n_atoms), dtype=np.float64) + distance_matrix: NDArray[np.float64] = np.zeros((n_atoms, n_atoms), dtype=np.float64) for i in range(n_atoms): diff = self.xyz[i] - self.xyz distance_matrix[i] = np.sqrt(np.sum(diff * diff, axis=1)) @@ -382,8 +379,8 @@ def set_element_radius(self, element: str, radius: float) -> None: def remove( self, - indices: Optional[List[int]] = None, - elements: Optional[List[str]] = None, + indices: Optional[list[int]] = None, + elements: Optional[list[str]] = None, inplace: bool = False, ) -> Optional["MolGraph"]: """Remove atoms by indices and/or elements. @@ -460,7 +457,7 @@ def to_networkx(self) -> nx.Graph: """ logger.debug("Creating NetworkX graph") - G = nx.Graph() + G: nx.Graph = nx.Graph() # Add nodes with attributes for atom in self.atoms: diff --git a/xyz2graph/molecule.py b/xyz2graph/molecule.py index 5db998a..eb2e8e7 100644 --- a/xyz2graph/molecule.py +++ b/xyz2graph/molecule.py @@ -4,7 +4,7 @@ """ from dataclasses import dataclass, field -from typing import FrozenSet, cast +from typing import cast import numpy as np from numpy.typing import NDArray @@ -51,12 +51,12 @@ def __post_init__(self) -> None: self.length = round(np.sqrt(dx * dx + dy * dy + dz * dz), 5) @property - def atoms(self) -> FrozenSet[Atom]: + def atoms(self) -> frozenset[Atom]: """Get the atoms involved in the bond as an order-independent set.""" return frozenset([self.atom1, self.atom2]) @property - def indices(self) -> FrozenSet[int]: + def indices(self) -> frozenset[int]: """Get indices of bonded atoms.""" return frozenset([self.atom1.index, self.atom2.index]) diff --git a/xyz2graph/visualization.py b/xyz2graph/visualization.py index 1f93828..687cf43 100644 --- a/xyz2graph/visualization.py +++ b/xyz2graph/visualization.py @@ -10,10 +10,7 @@ from typing import ( TYPE_CHECKING, Any, - Dict, - List, Optional, - Tuple, TypedDict, ) @@ -54,7 +51,7 @@ class ButtonsConfig(TypedDict): """Complete button configuration.""" appearance: ButtonAppearance - groups: Dict[str, ButtonPosition] + groups: dict[str, ButtonPosition] class AtomConfig(TypedDict): @@ -185,7 +182,7 @@ class VisualizationConfig(TypedDict): DEFAULT_BUTTON_X = 0.04 DEFAULT_LABEL_X = 0.0 -DEFAULT_BUTTON_GROUPS: Dict[str, ButtonPosition] = { +DEFAULT_BUTTON_GROUPS: dict[str, ButtonPosition] = { "atoms": { "button_x": DEFAULT_BUTTON_X, "button_y": 1.0, @@ -381,15 +378,15 @@ def watermark(self) -> WatermarkConfig: class AtomShape: """A class for managing atom shapes in the visualization.""" - atoms: List[Atom] - colors: Dict[str, str] + atoms: list[Atom] + colors: dict[str, str] def to_trace( self, config: AtomConfig, coordinate_round_digits: int, label_type: str = "elements", - ) -> List[go.Scatter3d]: + ) -> list[go.Scatter3d]: """Convert atoms to Plotly visualization traces. This method processes atoms into Plotly Scatter3d traces for visualization, @@ -413,7 +410,7 @@ def to_trace( coordinates, styling, and hover information for all atoms of that element. """ - element_groups: Dict[str, List[Atom]] = {} + element_groups: dict[str, list[Atom]] = {} for atom in self.atoms: element_groups.setdefault(atom.element, []).append(atom) @@ -468,11 +465,11 @@ class BondShape: including grouping bonds by type and applying visual styling. """ - bonds: List[Bond] + bonds: list[Bond] def to_trace( self, config: BondConfig, coordinate_round_digits: int, markdown_round_digits: int - ) -> List[go.Scatter3d]: + ) -> list[go.Scatter3d]: """Convert bonds to Plotly visualization traces. Processes molecular bonds into Plotly Scatter3d traces, grouping bonds by @@ -493,7 +490,7 @@ def to_trace( length labels are initially hidden and can be shown via UI controls. """ # Group bonds by type - bond_groups: Dict[str, List[Bond]] = {} + bond_groups: dict[str, list[Bond]] = {} for bond in self.bonds: elements = sorted([bond.atom1.element, bond.atom2.element]) bond_type = f"{elements[0]}-{elements[1]}" @@ -567,7 +564,7 @@ def __init__(self, buttons_config: ButtonsConfig, mol_graph: "MolGraph") -> None self.config = buttons_config self.mol_graph = mol_graph - def create_atom_buttons(self, trace_mapping: Dict[str, List[int]]) -> List[Dict[str, Any]]: + def create_atom_buttons(self, trace_mapping: dict[str, list[int]]) -> list[dict[str, Any]]: """Create buttons for controlling atom visualization. Args: @@ -613,7 +610,7 @@ def create_atom_buttons(self, trace_mapping: Dict[str, List[int]]) -> List[Dict[ return buttons - def create_bond_buttons(self, bond_traces: List[int]) -> List[Dict[str, Any]]: + def create_bond_buttons(self, bond_traces: list[int]) -> list[dict[str, Any]]: """Create buttons for controlling bond visualization. Args: @@ -640,15 +637,15 @@ def create_bond_buttons(self, bond_traces: List[int]) -> List[Dict[str, Any]]: }, ] - def create_grid_buttons(self) -> List[Dict[str, Any]]: + def create_grid_buttons(self) -> list[dict[str, Any]]: """Create buttons for controlling grid visualization. Returns: List of button configurations. """ - def make_args(show: bool) -> Dict[str, Any]: - args: Dict[str, Any] = {} + def make_args(show: bool) -> dict[str, Any]: + args: dict[str, Any] = {} for axis in ["xaxis", "yaxis", "zaxis"]: args[f"scene.{axis}.showbackground"] = show args[f"scene.{axis}.gridcolor"] = "lightgray" if show else "white" @@ -662,8 +659,8 @@ def make_args(show: bool) -> Dict[str, Any]: ] def create_button_group( - self, group_name: str, buttons: List[Dict[str, Any]], active_index: int = 0 - ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + self, group_name: str, buttons: list[dict[str, Any]], active_index: int = 0 + ) -> tuple[dict[str, Any], dict[str, Any]]: """Create a button group with its associated title annotation. Creates a configured button group that includes both the interactive buttons and @@ -732,11 +729,11 @@ def __init__(self, buttons_config: ButtonsConfig, mol_graph: "MolGraph") -> None """ self.button_factory = ButtonFactory(buttons_config, mol_graph) self.mol_graph = mol_graph - self.atom_traces: Dict[str, List[int]] = {} - self.bond_traces: List[int] = [] + self.atom_traces: dict[str, list[int]] = {} + self.bond_traces: list[int] = [] def update_trace_indices( - self, atom_traces: Dict[str, List[int]], bond_traces: List[int] + self, atom_traces: dict[str, list[int]], bond_traces: list[int] ) -> None: """Update the indices of atom and bond traces. @@ -747,7 +744,7 @@ def update_trace_indices( self.atom_traces = atom_traces self.bond_traces = bond_traces - def create_all_controls(self) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: + def create_all_controls(self) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: """Create all visualization controls and their annotations. Generates three types of controls: @@ -810,7 +807,7 @@ def __init__(self, mol_graph: "MolGraph", config: Optional[VisualizationConfig] mol_graph=self.mol_graph, ) - def _create_axis_config(self) -> Dict[str, Dict[str, Any]]: + def _create_axis_config(self) -> dict[str, dict[str, Any]]: """Create the configuration dictionary for scene axes. Generates a configuration dictionary for the x, y, and z axes of the 3D @@ -848,7 +845,7 @@ def _create_annotation( font_color: str, xanchor: str = "left", yanchor: str = "bottom", - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Create a standardized annotation dictionary. Args: