Skip to content

Commit

Permalink
Test with Pint 0.23, add downstream testing (#83)
Browse files Browse the repository at this point in the history
* Update linters

* Test with Pint 0.23

* Add downstream package for testing

* Support Python 3.9

* Upload to CodeCov with token
  • Loading branch information
mattwthompson authored Mar 25, 2024
1 parent cfdb193 commit b8fdc8b
Show file tree
Hide file tree
Showing 8 changed files with 397 additions and 152 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ jobs:
os: [macOS-latest, ubuntu-latest]
openmm: ["true", "false"]
python-version: ["3.9", "3.10", "3.11", "3.12"]
pint-version: ["0.21", "0.22"]
pint-version: ["0.21", "0.22", "0.23"]

env:
CI_OS: ${{ matrix.os }}
PYVER: ${{ matrix.python-version }}
PYTEST_ARGS: -v -nauto
PYTEST_ARGS: -v -n logical
COV: --cov=openff/units --cov-report=xml --cov-config=setup.cfg --cov-append

steps:
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
fi
- name: Install package
run: python -m pip install -e .
run: python -m pip install -e . downstream_dummy/

- name: Run mypy
if: ${{ matrix.python-version == 'false' }}
Expand All @@ -69,12 +69,16 @@ jobs:
pytest $PYTEST_ARGS $COV openff/units/_tests/
- name: Run dummy package tests
run: pytest $PYTEST_ARGS downstream_dummy/tests/

- name: Run docexamples
if: ${{ matrix.openmm == 'true' }}
run: pytest --doctest-modules $PYTEST_ARGS $COV openff --ignore=openff/units/_tests

- name: Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
fail_ci_if_error: false
7 changes: 1 addition & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@ ci:

repos:
- repo: https://github.com/psf/black
rev: 24.1.1
rev: 24.3.0
hooks:
- id: black
files: ^openff
- id: black-jupyter
files: ^examples/((?!deprecated).)*$
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
files: ^openff
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
files: ^openff
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import os
import sys
from importlib.util import find_spec as find_import_spec

sys.path.insert(0, os.path.abspath("."))

Expand Down Expand Up @@ -114,7 +115,6 @@
# sphinx-notfound-page
# https://github.com/readthedocs/sphinx-notfound-page
# Renders a 404 page with absolute links
from importlib.util import find_spec as find_import_spec

if find_import_spec("notfound"):
extensions.append("notfound.extension")
Expand Down
144 changes: 144 additions & 0 deletions downstream_dummy/dummy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""
See openff/toolkit/utils/utils.py
"""

import functools
from typing import Iterable, Union

from openff.units import Quantity, unit


def string_to_quantity(quantity_string) -> Union[str, int, float, Quantity]:
"""Attempt to parse a string into a unit.Quantity.
Note that dimensionless floats and ints are returns as floats or ints, not Quantity objects.
"""

from tokenize import TokenError

from pint import UndefinedUnitError

try:
quantity = Quantity(quantity_string)
except (TokenError, UndefinedUnitError):
return quantity_string

# TODO: Should intentionally unitless array-likes be Quantity objects
# or their raw representation?
if (quantity.units == unit.dimensionless) and isinstance(quantity.m, (int, float)):
return quantity.m
else:
return quantity


def convert_all_strings_to_quantity(
smirnoff_data: dict,
ignore_keys: Iterable[str] = tuple(),
):
"""
Traverses a SMIRNOFF data structure, attempting to convert all
quantity-defining strings into openff.units.unit.Quantity objects.
Integers and floats are ignored and not converted into a dimensionless
``openff.units.unit.Quantity`` object.
Parameters
----------
smirnoff_data
A hierarchical dict structured in compliance with the SMIRNOFF spec
ignore_keys
A list of keys to skip when converting strings to quantities
Returns
-------
converted_smirnoff_data
A hierarchical dict structured in compliance with the SMIRNOFF spec,
with quantity-defining strings converted to openff.units.unit.Quantity objects
"""
from pint import DefinitionSyntaxError

if isinstance(smirnoff_data, dict):
for key, value in smirnoff_data.items():
if key in ignore_keys:
smirnoff_data[key] = value
else:
smirnoff_data[key] = convert_all_strings_to_quantity(
value,
ignore_keys=ignore_keys,
)
obj_to_return = smirnoff_data

elif isinstance(smirnoff_data, list):
for index, item in enumerate(smirnoff_data):
smirnoff_data[index] = convert_all_strings_to_quantity(
item,
ignore_keys=ignore_keys,
)
obj_to_return = smirnoff_data

elif isinstance(smirnoff_data, int) or isinstance(smirnoff_data, float):
obj_to_return = smirnoff_data

else:
try:
obj_to_return = object_to_quantity(smirnoff_data)
except (TypeError, DefinitionSyntaxError):
obj_to_return = smirnoff_data

return obj_to_return


@functools.singledispatch
def object_to_quantity(object):
"""
Attempts to turn the provided object into openmm.unit.Quantity(s).
Can handle float, int, strings, quantities, or iterators over
the same. Raises an exception if unable to convert all inputs.
Parameters
----------
object
The object to convert to a ``openmm.unit.Quantity`` object.
Returns
-------
converted_object
"""
# If we can't find a custom type, we treat this as a generic iterator.
return [object_to_quantity(sub_obj) for sub_obj in object]


@object_to_quantity.register(Quantity)
def _(obj):
return obj


@object_to_quantity.register(str)
def _(obj):
import pint

try:
return string_to_quantity(obj)
except pint.errors.UndefinedUnitError:
raise ValueError


@object_to_quantity.register(int)
@object_to_quantity.register(float)
def _(obj):
return Quantity(obj)


try:
import openmm

from openff.units.openmm import from_openmm

@object_to_quantity.register(openmm.unit.Quantity)
def _(obj):
return from_openmm(obj)

except ImportError:
pass # pragma: nocover
10 changes: 10 additions & 0 deletions downstream_dummy/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
A test package used to ensure that downstream packages can do basic things with openff-units.
"""

from setuptools import setup

setup(
version="0.0.0",
name="openff-units-downstream-dummy",
)
37 changes: 37 additions & 0 deletions downstream_dummy/tests/test_dummy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Union

import pytest
from dummy import object_to_quantity

from openff.units import Quantity, Unit


def test_function_can_be_defined():
"""
Just make sure a function can be defined using these classes.
Safeguard against i.e. https://github.com/openforcefield/openff-toolkit/issues/1632
Type checkers might not be happy.
"""

def dummy_function(
unit: Unit,
quantity: Quantity,
extra: Union[Unit, Quantity, str, None] = None,
):
return f"{unit} {quantity} {extra}"


@pytest.mark.parametrize(
"input, output",
[
("1.0 * kilocalories_per_mole", Quantity(1.0, "kilocalories_per_mole")),
(Quantity("2.0 * angstrom"), Quantity(2.0, "angstrom")),
(3.0 * Unit("nanometer"), Quantity(3.0, "nanometer")),
(4, Quantity(4.0)),
(5.0, Quantity(5.0)),
],
)
def test_object_to_quantity(input, output):
assert object_to_quantity(input) == output
25 changes: 12 additions & 13 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,32 @@
openff-toolkit
A common units module for the OpenFF software stack
"""

import sys
from setuptools import setup, find_namespace_packages

from setuptools import find_namespace_packages, setup

import versioneer

short_description = __doc__.split("\n")

needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv)
pytest_runner = ['pytest-runner'] if needs_pytest else []
needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv)
pytest_runner = ["pytest-runner"] if needs_pytest else []

try:
with open("README.md", "r") as handle:
long_description = handle.read()
except:
long_description = "\n".join(short_description[2:])
long_description = open("README.md", "r").read()


setup(
name='openff-units',
author='Open Force Field Initiative',
author_email='[email protected]',
name="openff-units",
author="Open Force Field Initiative",
author_email="[email protected]",
description=short_description[0],
long_description=long_description,
long_description_content_type="text/markdown",
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
license='MIT',
packages=find_namespace_packages(include=['openff.*']),
license="MIT",
packages=find_namespace_packages(include=["openff.*"]),
package_data={"openff.units": ["py.typed"]},
include_package_data=True,
setup_requires=[] + pytest_runner,
Expand Down
Loading

0 comments on commit b8fdc8b

Please sign in to comment.