Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion devtools/conda-envs/docs-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ channels:
- openeye
dependencies:
# Basic Python dependencies
- python=3.11.0
- python>=3.11
- pip

# Unit testing
Expand Down
6 changes: 3 additions & 3 deletions devtools/conda-envs/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions devtools/conda-envs/test-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
25 changes: 16 additions & 9 deletions polymerist/genutils/importutils/pkginspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down
4 changes: 2 additions & 2 deletions polymerist/polymers/building/sequencing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}]')
Expand Down
87 changes: 45 additions & 42 deletions polymerist/tests/genutils/importutils/test_pkginspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
__email__ = '[email protected]'

from types import ModuleType
from dataclasses import dataclass

import pytest
from pathlib import Path
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.
Expand Down
Loading