diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index c9d048a4..c803ec69 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -30,7 +30,7 @@ jobs: strategy: matrix: os: [macOS-latest, ubuntu-latest] #, windows-latest] # AmberTools is not currently installable on Windows - python-version: ["3.11"] #, "3.12"] # intend to support more recent Python version eventually, but tooling is built around 3.11 only for now + python-version: ["3.11", "3.12"] # intend to support more recent Python version eventually, but tooling is built around 3.11 only for now fail-fast: false steps: diff --git a/devtools/conda-envs/docs-env.yml b/devtools/conda-envs/docs-env.yml index f89e98dc..ac646e10 100644 --- a/devtools/conda-envs/docs-env.yml +++ b/devtools/conda-envs/docs-env.yml @@ -4,7 +4,7 @@ channels: - openeye dependencies: # Basic Python dependencies - - python=3.11.0 + - python>=3.11 - pip # Unit testing diff --git a/devtools/conda-envs/release-build.yml b/devtools/conda-envs/release-build.yml index 948cd5e2..6b086272 100644 --- a/devtools/conda-envs/release-build.yml +++ b/devtools/conda-envs/release-build.yml @@ -4,9 +4,9 @@ channels: - openeye dependencies: # Basic Python dependencies - - python=3.11.0 + - python>=3.11.0 - pip - - setuptools<82 # to avoid ImportErrors from pkg_resources deprecation + - setuptools # Unit testing - pytest @@ -35,7 +35,7 @@ dependencies: - rdkit - mbuild - openbabel - - packmol<=20.15.1 # DEVNOTE: have no idea why, but versions tested later than this release (at least 20.16.1 and 21.0.4) introduce PDB CONECT errors where there were previously none + - packmol - openeye-toolkits # TODO: consider making this optional? # MD engines diff --git a/devtools/conda-envs/test-env.yml b/devtools/conda-envs/test-env.yml index 9fe1f7de..68a00cb6 100644 --- a/devtools/conda-envs/test-env.yml +++ b/devtools/conda-envs/test-env.yml @@ -4,9 +4,9 @@ channels: - openeye dependencies: # Basic Python dependencies - - python=3.11.0 + - python>=3.11.0 - pip - - setuptools<82 # to avoid ImportErrors from pkg_resources deprecation + - setuptools # Unit testing - pytest @@ -28,7 +28,8 @@ dependencies: - rdkit - mbuild - openbabel - - packmol<=20.15.1 # DEVNOTE: have no idea why, but versions tested later than this release (at least 20.16.1 and 21.0.4) introduce PDB CONECT errors where there were previously none + # - packmol<=20.15.1 # DEVNOTE: have no idea why, but versions tested later than this release (at least 20.16.1 and 21.0.4) introduce PDB CONECT errors where there were previously none + - packmol - openeye-toolkits # TODO: consider making this optional? # MD engines diff --git a/polymerist/genutils/importutils/pkginspect.py b/polymerist/genutils/importutils/pkginspect.py index d8d6b29b..137857dd 100644 --- a/polymerist/genutils/importutils/pkginspect.py +++ b/polymerist/genutils/importutils/pkginspect.py @@ -11,29 +11,36 @@ Package, files as get_package_path ) -from importlib.resources._common import get_package, from_package, resolve +from importlib.resources._common import resolve + +ModuleLike = Union[str, ModuleType, Package] # CHECKING PACKAGE AND MODULE STATUS -def is_module(module : Package) -> bool: +def is_module(module : ModuleLike) -> bool: '''Determine whether a given Package-like (i.e. str or ModuleType) is a valid Python module This will return True for packages, bottom-level modules (i.e. *.py) and Python scripts''' try: - resolve(module) - return True + module_loaded : ModuleType = resolve(module) + return True # enough to check that module is loadable as ModuleType except ModuleNotFoundError: return False -def is_package(package : Package) -> bool: +def is_package(package : ModuleLike) -> bool: '''Determine whether a given Package-like (i.e. str or ModuleType) is a valid Python package''' try: - get_package(package) - return True - except (ModuleNotFoundError, TypeError): + module_loaded : ModuleType = resolve(package) + if (module_spec := module_loaded.__spec__) is None: + return False + return (module_spec.submodule_search_locations is not None) + # per importlib docs : "[submodule_search_locations] should be set to None for non-package modules" + # (https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations) + except (ModuleNotFoundError): + # DEV 05/15/2026 - removed TypeError, as this was only raised by the deprecated _common.get_package function return False # EXTRACTING MODULE NAMING INFO -def flexible_module_pass(module : Union[str, Path, ModuleType]) -> ModuleType: # TODO: extend this to decorator +def flexible_module_pass(module : ModuleLike | Path) -> ModuleType: # TODO: extend this to decorator '''Flexible interface for supplying a ModuleType object as an argument Allows for passing a name (either module name or string path), Path location, or a module proper''' if isinstance(module, (str, ModuleType)): diff --git a/polymerist/polymers/building/sequencing.py b/polymerist/polymers/building/sequencing.py index cd52c698..1c66b966 100644 --- a/polymerist/polymers/building/sequencing.py +++ b/polymerist/polymers/building/sequencing.py @@ -7,7 +7,7 @@ LOGGER = logging.getLogger(__name__) from typing import Iterable, Optional -from dataclasses import dataclass, field, asdict +from dataclasses import dataclass, asdict from ...genutils.textual.substrings import shortest_repeating_substring, repeat_string_to_length from ...genutils.fileutils.jsonio.jsonify import make_jsonifiable @@ -174,7 +174,7 @@ def describe_order(self, end_group_names : Optional[Iterable[str]]=None, default if (num_names_provided := len(end_group_names)) != self.n_repeat_units_terminal: # DEV: consider supporting filling in missing names with default in future raise IndexError(f'Defined sequence info with {self.n_repeat_units_terminal} end groups, but only provided names for {num_names_provided}') - # Insert middle omnomer parts as necessary + # Insert middle monomer parts as necessary sequence_middle = [] if self.n_full_periods != 0: ## Whole sequence strings sequence_middle.append(f'{self.n_full_periods}*[{self.sequence_kernel}]') diff --git a/polymerist/tests/genutils/importutils/test_pkginspect.py b/polymerist/tests/genutils/importutils/test_pkginspect.py index 81627bf9..73bc43db 100644 --- a/polymerist/tests/genutils/importutils/test_pkginspect.py +++ b/polymerist/tests/genutils/importutils/test_pkginspect.py @@ -4,6 +4,7 @@ __email__ = 'timotej.bernat@colorado.edu' from types import ModuleType +from dataclasses import dataclass import pytest from pathlib import Path @@ -16,64 +17,66 @@ from polymerist import tests - # TABULATED EXPECTED TESTS OUTPUTS -non_module_types = [ # types that are obviously not modules OR packages, and which should fail - bool, int, float, complex, tuple, list, dict, set, # str, Path # str and Path need to be tested separately -] - -are_modules = [ - ('--not_a_module--', False), # deliberately weird to ensure this never accidentally clashes with a legit module name - (math, True), - ('math', True), # test that the string -> module resolver also works as intended - (json, True), - ('json', True), - (json.decoder, True), - ('json.decoder', True), - (polymerist, True), - ('polymerist.polymerist', True), - (genutils, True), - ('polymerist.genutils', True), -] - -are_packages = [ - ('--not_a_package--', False), # deliberately weird to ensure this never accidentally clashes with a legit module name - (math, False), - ('math', False), # test that the string -> module resolver also works as intended - (json, True), - ('json', True), - (json.decoder, False), - ('json.decoder', False), - (polymerist, False), - ('polymerist.polymerist', False), - (genutils, True), - ('polymerist.genutils', True), -] +def non_module_types() ->list[type]: + '''Types that are obviously not modules OR packages, and which should fail''' + return [ + bool, int, float, complex, tuple, list, dict, set, + # str, Path # str and Path need to be tested separately + ] + +@dataclass(frozen=True) +class ModuleExample: + '''For encapsulating package and module check tests''' + resource : str | ModuleType + is_module : bool + is_package : bool + +def module_examples() -> tuple[ModuleExample, ...]: + ''' + Module-like objects labelled with whether they + are modules and whether they are packages + ''' + return ( + # deliberately weird to ensure this never accidentally clashes with a legit module name + ModuleExample('--not_a_module--', False, False), + ModuleExample(math, True, False), + # test that the string -> module resolver also works as intended + ModuleExample('math', True, False), + ModuleExample(json, True, True), + ModuleExample('json', True, True), + ModuleExample(json.decoder, True, False), + ModuleExample('json.decoder', True, False), + ModuleExample(polymerist, True, False), + ModuleExample('polymerist.polymerist', True, False), + ModuleExample(genutils, True, True), + ModuleExample('polymerist.genutils', True, True), + ) # MODULE AND PACKAGE PERCEPTION -@pytest.mark.parametrize('module, expected_output', are_modules) -def test_is_module(module : ModuleType, expected_output : bool) -> None: +@pytest.mark.parametrize('module_example', module_examples()) +def test_is_module(module_example : ModuleExample) -> None: '''See if Python module perception behaves as expected''' - assert pkginspect.is_module(module) == expected_output + assert pkginspect.is_module(module_example.resource) == module_example.is_module -@pytest.mark.parametrize('non_module_type', non_module_types) +@pytest.mark.parametrize('non_module_type', non_module_types()) def test_is_module_fail_on_invalid_types(non_module_type : type) -> None: '''check that module perception fails on invalid inputs''' with pytest.raises(AttributeError) as err_info: instance = non_module_type() # create a default instance _ = pkginspect.is_module(instance) -@pytest.mark.parametrize('module, expected_output', are_packages) -def test_is_package(module : ModuleType, expected_output : bool) -> None: +@pytest.mark.parametrize('module_example', module_examples()) +def test_is_package(module_example : ModuleExample) -> None: '''See if Python package perception behaves as expected''' - assert pkginspect.is_package(module) == expected_output + assert pkginspect.is_package(module_example.resource) == module_example.is_package -@pytest.mark.parametrize('non_module_type', non_module_types) # NOTE: these args are in fact deliberately NOT renamed to ".*package" from ".*module" -def test_is_module_fail_on_invalid_types(non_module_type : type) -> None: +@pytest.mark.parametrize('non_package_type', non_module_types()) +def test_is_package_fail_on_invalid_types(non_package_type : type) -> None: '''check that package perception fails on invalid inputs''' with pytest.raises(AttributeError) as err_info: - instance = non_module_type() # create a default instance + instance = non_package_type() # create a default instance _ = pkginspect.is_package(instance) # FETCHING DATA FROM PACKAGES diff --git a/pyproject.toml b/pyproject.toml index cc2f8517..ab08b95f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,12 +18,12 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", ] -requires-python = "~=3.11" +requires-python = ">=3.11" # Declare any run-time dependencies that should be installed with the package. dependencies = [ "importlib-resources", # added to allow incorporation of `data` later on - "setuptools<82.0.0", # added to avoid CI errors due to pkg_resources deprecation - "numpy<2.0.0", # DEV: pinning this, as leaving it unpinned causes errors when importing mbuild (deprecated numpy.vecdot function) + "setuptools", # added to avoid CI errors due to pkg_resources deprecation + "numpy>=2.0.0", # DEV: pinning this, as leaving it unpinned causes errors when importing mbuild (deprecated numpy.vecdot function) "scipy", "pandas", "rich", @@ -36,7 +36,8 @@ dependencies = [ "openmm", # DEV: as of 08/18/2025, latest stable version of lammps shipped w/ binaries on conda # lammps-dependent code is unrunnable (citing MPI OSError) for more recent builds (i.e. 2025.MM.DD) - "lammps<=2024.08.29", + # "lammps<=2024.08.29", + "lammps", ] # Update the urls once the hosting is set up.