acellera-openff-utilities 0.1.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. acellera_openff_utilities-0.1.16/LICENSE +22 -0
  2. acellera_openff_utilities-0.1.16/MANIFEST.in +6 -0
  3. acellera_openff_utilities-0.1.16/PKG-INFO +28 -0
  4. acellera_openff_utilities-0.1.16/README.md +18 -0
  5. acellera_openff_utilities-0.1.16/acellera_openff_utilities.egg-info/PKG-INFO +28 -0
  6. acellera_openff_utilities-0.1.16/acellera_openff_utilities.egg-info/SOURCES.txt +25 -0
  7. acellera_openff_utilities-0.1.16/acellera_openff_utilities.egg-info/dependency_links.txt +1 -0
  8. acellera_openff_utilities-0.1.16/acellera_openff_utilities.egg-info/top_level.txt +3 -0
  9. acellera_openff_utilities-0.1.16/devtools/scripts/assert_openeye_not_found.py +6 -0
  10. acellera_openff_utilities-0.1.16/docs/conf.py +173 -0
  11. acellera_openff_utilities-0.1.16/openff/utilities/__init__.py +30 -0
  12. acellera_openff_utilities-0.1.16/openff/utilities/_tests/__init__.py +0 -0
  13. acellera_openff_utilities-0.1.16/openff/utilities/_tests/test_exceptions.py +17 -0
  14. acellera_openff_utilities-0.1.16/openff/utilities/_tests/test_provenance.py +54 -0
  15. acellera_openff_utilities-0.1.16/openff/utilities/_tests/test_star_import.py +1 -0
  16. acellera_openff_utilities-0.1.16/openff/utilities/_tests/test_testing_utilities.py +16 -0
  17. acellera_openff_utilities-0.1.16/openff/utilities/_tests/test_utilities.py +181 -0
  18. acellera_openff_utilities-0.1.16/openff/utilities/data/data.dat +2 -0
  19. acellera_openff_utilities-0.1.16/openff/utilities/data/more/more.dat +0 -0
  20. acellera_openff_utilities-0.1.16/openff/utilities/exceptions.py +52 -0
  21. acellera_openff_utilities-0.1.16/openff/utilities/provenance.py +66 -0
  22. acellera_openff_utilities-0.1.16/openff/utilities/py.typed +0 -0
  23. acellera_openff_utilities-0.1.16/openff/utilities/testing.py +54 -0
  24. acellera_openff_utilities-0.1.16/openff/utilities/utilities.py +261 -0
  25. acellera_openff_utilities-0.1.16/openff/utilities/warnings.py +4 -0
  26. acellera_openff_utilities-0.1.16/pyproject.toml +85 -0
  27. acellera_openff_utilities-0.1.16/setup.cfg +4 -0
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2021 The Open Force Field Initiative
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include LICENSE
2
+ include MANIFEST.in
3
+ include CODE_OF_CONDUCT.md
4
+
5
+ graft openff
6
+ global-exclude *.py[cod] __pycache__ *.so
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: acellera-openff-utilities
3
+ Version: 0.1.16
4
+ Summary: A collection of miscellaneous utility functions used throughout the OpenFF stack
5
+ Author-email: Open Force Field Initiative <info@openforcefield.org>
6
+ License: MIT
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ openff-utilities
12
+ ==============================
13
+ [//]: # (Badges)
14
+ [![GitHub Actions Build Status](https://github.com/openforcefield/openff-utilities/workflows/CI/badge.svg)](https://github.com/openforcefield/openff-utilities/actions?query=workflow%3ACI)
15
+ [![codecov](https://codecov.io/gh/openforcefield/openff-utilities/branch/main/graph/badge.svg)](https://codecov.io/gh/openforcefield/openff-utilities/branch/main)
16
+
17
+
18
+ A collection of miscellaneous utility functions used throughout the OpenFF stack
19
+
20
+ ### Copyright
21
+
22
+ Copyright (c) 2021, The Open Force Field Initiative
23
+
24
+
25
+ #### Acknowledgements
26
+
27
+ Project based on the
28
+ [Computational Molecular Science Python Cookiecutter](https://github.com/molssi/cookiecutter-cms) version 1.5.
@@ -0,0 +1,18 @@
1
+ openff-utilities
2
+ ==============================
3
+ [//]: # (Badges)
4
+ [![GitHub Actions Build Status](https://github.com/openforcefield/openff-utilities/workflows/CI/badge.svg)](https://github.com/openforcefield/openff-utilities/actions?query=workflow%3ACI)
5
+ [![codecov](https://codecov.io/gh/openforcefield/openff-utilities/branch/main/graph/badge.svg)](https://codecov.io/gh/openforcefield/openff-utilities/branch/main)
6
+
7
+
8
+ A collection of miscellaneous utility functions used throughout the OpenFF stack
9
+
10
+ ### Copyright
11
+
12
+ Copyright (c) 2021, The Open Force Field Initiative
13
+
14
+
15
+ #### Acknowledgements
16
+
17
+ Project based on the
18
+ [Computational Molecular Science Python Cookiecutter](https://github.com/molssi/cookiecutter-cms) version 1.5.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: acellera-openff-utilities
3
+ Version: 0.1.16
4
+ Summary: A collection of miscellaneous utility functions used throughout the OpenFF stack
5
+ Author-email: Open Force Field Initiative <info@openforcefield.org>
6
+ License: MIT
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ openff-utilities
12
+ ==============================
13
+ [//]: # (Badges)
14
+ [![GitHub Actions Build Status](https://github.com/openforcefield/openff-utilities/workflows/CI/badge.svg)](https://github.com/openforcefield/openff-utilities/actions?query=workflow%3ACI)
15
+ [![codecov](https://codecov.io/gh/openforcefield/openff-utilities/branch/main/graph/badge.svg)](https://codecov.io/gh/openforcefield/openff-utilities/branch/main)
16
+
17
+
18
+ A collection of miscellaneous utility functions used throughout the OpenFF stack
19
+
20
+ ### Copyright
21
+
22
+ Copyright (c) 2021, The Open Force Field Initiative
23
+
24
+
25
+ #### Acknowledgements
26
+
27
+ Project based on the
28
+ [Computational Molecular Science Python Cookiecutter](https://github.com/molssi/cookiecutter-cms) version 1.5.
@@ -0,0 +1,25 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ acellera_openff_utilities.egg-info/PKG-INFO
6
+ acellera_openff_utilities.egg-info/SOURCES.txt
7
+ acellera_openff_utilities.egg-info/dependency_links.txt
8
+ acellera_openff_utilities.egg-info/top_level.txt
9
+ devtools/scripts/assert_openeye_not_found.py
10
+ docs/conf.py
11
+ openff/utilities/__init__.py
12
+ openff/utilities/exceptions.py
13
+ openff/utilities/provenance.py
14
+ openff/utilities/py.typed
15
+ openff/utilities/testing.py
16
+ openff/utilities/utilities.py
17
+ openff/utilities/warnings.py
18
+ openff/utilities/_tests/__init__.py
19
+ openff/utilities/_tests/test_exceptions.py
20
+ openff/utilities/_tests/test_provenance.py
21
+ openff/utilities/_tests/test_star_import.py
22
+ openff/utilities/_tests/test_testing_utilities.py
23
+ openff/utilities/_tests/test_utilities.py
24
+ openff/utilities/data/data.dat
25
+ openff/utilities/data/more/more.dat
@@ -0,0 +1,6 @@
1
+ try:
2
+ from openeye import oechem
3
+
4
+ raise Exception("OpenEye Toolkits ARE installed")
5
+ except ModuleNotFoundError:
6
+ pass
@@ -0,0 +1,173 @@
1
+ #
2
+ # Configuration file for the Sphinx documentation builder.
3
+ #
4
+ # This file does only contain a selection of the most common options. For a
5
+ # full list see the documentation:
6
+ # http://www.sphinx-doc.org/en/stable/config
7
+
8
+ # -- Path setup --------------------------------------------------------------
9
+
10
+ # If extensions (or modules to document with autodoc) are in another directory,
11
+ # add these directories to sys.path here. If the directory is relative to the
12
+ # documentation root, use os.path.abspath to make it absolute, like shown here.
13
+
14
+ # Incase the project was not installed
15
+ import os
16
+ import sys
17
+
18
+ sys.path.insert(0, os.path.abspath(".."))
19
+
20
+
21
+ # -- Project information -----------------------------------------------------
22
+
23
+ project = "openff-utilities"
24
+ copyright = (
25
+ "2021, The Open Force Field Initiative. Project structure based on the "
26
+ "Computational Molecular Science Python Cookiecutter version 1.5"
27
+ )
28
+ author = "The Open Force Field Initiative"
29
+
30
+ # The short X.Y version
31
+ version = ""
32
+ # The full version, including alpha/beta/rc tags
33
+ release = ""
34
+
35
+
36
+ # -- General configuration ---------------------------------------------------
37
+
38
+ # If your documentation needs a minimal Sphinx version, state it here.
39
+ #
40
+ # needs_sphinx = '1.0'
41
+
42
+ # Add any Sphinx extension module names here, as strings. They can be
43
+ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
44
+ # ones.
45
+ extensions = [
46
+ "sphinx.ext.autosummary",
47
+ "sphinx.ext.autodoc",
48
+ "sphinx.ext.mathjax",
49
+ "sphinx.ext.viewcode",
50
+ "sphinx.ext.napoleon",
51
+ "sphinx.ext.intersphinx",
52
+ "sphinx.ext.extlinks",
53
+ ]
54
+
55
+ autosummary_generate = True
56
+ napoleon_google_docstring = False
57
+ napoleon_use_param = False
58
+ napoleon_use_ivar = True
59
+
60
+ # Add any paths that contain templates here, relative to this directory.
61
+ templates_path = ["_templates"]
62
+
63
+ # The suffix(es) of source filenames.
64
+ # You can specify multiple suffix as a list of string:
65
+ #
66
+ # source_suffix = ['.rst', '.md']
67
+ source_suffix = ".rst"
68
+
69
+ # The master toctree document.
70
+ master_doc = "index"
71
+
72
+ # The language for content autogenerated by Sphinx. Refer to documentation
73
+ # for a list of supported languages.
74
+ #
75
+ # This is also used if you do content translation via gettext catalogs.
76
+ # Usually you set "language" from the command line for these cases.
77
+ language = None
78
+
79
+ # List of patterns, relative to source directory, that match files and
80
+ # directories to ignore when looking for source files.
81
+ # This pattern also affects html_static_path and html_extra_path .
82
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
83
+
84
+ # The name of the Pygments (syntax highlighting) style to use.
85
+ pygments_style = "default"
86
+
87
+
88
+ # -- Options for HTML output -------------------------------------------------
89
+
90
+ # The theme to use for HTML and HTML Help pages. See the documentation for
91
+ # a list of builtin themes.
92
+ #
93
+ html_theme = "sphinx_rtd_theme"
94
+
95
+ # Theme options are theme-specific and customize the look and feel of a theme
96
+ # further. For a list of options available for each theme, see the
97
+ # documentation.
98
+ #
99
+ # html_theme_options = {}
100
+
101
+ # Add any paths that contain custom static files (such as style sheets) here,
102
+ # relative to this directory. They are copied after the builtin static files,
103
+ # so a file named "default.css" will overwrite the builtin "default.css".
104
+ html_static_path = ["_static"]
105
+
106
+ # Custom sidebar templates, must be a dictionary that maps document names
107
+ # to template names.
108
+ #
109
+ # The default sidebars (for documents that don't match any pattern) are
110
+ # defined by theme itself. Builtin themes are using these templates by
111
+ # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
112
+ # 'searchbox.html']``.
113
+ #
114
+ # html_sidebars = {}
115
+
116
+
117
+ # -- Options for HTMLHelp output ---------------------------------------------
118
+
119
+ # Output file base name for HTML help builder.
120
+ htmlhelp_basename = "openffdoc"
121
+
122
+
123
+ # -- Options for LaTeX output ------------------------------------------------
124
+
125
+ latex_elements = {
126
+ # The paper size ('letterpaper' or 'a4paper').
127
+ #
128
+ # 'papersize': 'letterpaper',
129
+ # The font size ('10pt', '11pt' or '12pt').
130
+ #
131
+ # 'pointsize': '10pt',
132
+ # Additional stuff for the LaTeX preamble.
133
+ #
134
+ # 'preamble': '',
135
+ # Latex figure (float) alignment
136
+ #
137
+ # 'figure_align': 'htbp',
138
+ }
139
+
140
+ # Grouping the document tree into LaTeX files. List of tuples
141
+ # (source start file, target name, title,
142
+ # author, documentclass [howto, manual, or own class]).
143
+ latex_documents = [
144
+ (master_doc, "openff.tex", "openff-utilities Documentation", "openff", "manual"),
145
+ ]
146
+
147
+
148
+ # -- Options for manual page output ------------------------------------------
149
+
150
+ # One entry per manual page. List of tuples
151
+ # (source start file, name, description, authors, manual section).
152
+ man_pages = [(master_doc, "openff", "openff-utilities Documentation", [author], 1)]
153
+
154
+
155
+ # -- Options for Texinfo output ----------------------------------------------
156
+
157
+ # Grouping the document tree into Texinfo files. List of tuples
158
+ # (source start file, target name, title, author,
159
+ # dir menu entry, description, category)
160
+ texinfo_documents = [
161
+ (
162
+ master_doc,
163
+ "openff",
164
+ "openff-utilities Documentation",
165
+ author,
166
+ "openff",
167
+ "A collection of miscellaneous utility functions used throughout the OpenFF stack",
168
+ "Miscellaneous",
169
+ ),
170
+ ]
171
+
172
+
173
+ # -- Extension configuration -------------------------------------------------
@@ -0,0 +1,30 @@
1
+ from importlib.metadata import version
2
+
3
+ from openff.utilities.exceptions import MissingOptionalDependencyError
4
+ from openff.utilities.provenance import get_ambertools_version
5
+ from openff.utilities.testing import skip_if_missing, skip_if_missing_exec
6
+ from openff.utilities.utilities import (
7
+ get_data_dir_path,
8
+ get_data_file_path,
9
+ has_executable,
10
+ has_package,
11
+ requires_oe_module,
12
+ requires_package,
13
+ temporary_cd,
14
+ )
15
+
16
+ __all__ = (
17
+ "MissingOptionalDependencyError",
18
+ "get_ambertools_version",
19
+ "get_data_dir_path",
20
+ "get_data_file_path",
21
+ "has_executable",
22
+ "has_package",
23
+ "requires_oe_module",
24
+ "requires_package",
25
+ "skip_if_missing",
26
+ "skip_if_missing_exec",
27
+ "temporary_cd",
28
+ )
29
+
30
+ __version__ = version("openff.utilities")
@@ -0,0 +1,17 @@
1
+ import pytest
2
+
3
+ from openff.utilities.exceptions import MissingOptionalDependencyError, OpenFFError
4
+
5
+
6
+ def test_exceptions():
7
+ with pytest.raises(
8
+ MissingOptionalDependencyError,
9
+ match="The required foobar module could not be imported.*Try installing.*conda-forge.*",
10
+ ):
11
+ raise MissingOptionalDependencyError(library_name="foobar")
12
+
13
+ with pytest.raises(MissingOptionalDependencyError, match=".*barbaz.*missing license."):
14
+ raise MissingOptionalDependencyError(library_name="barbaz", license_issue=True)
15
+
16
+ with pytest.raises(OpenFFError):
17
+ raise MissingOptionalDependencyError("numpy")
@@ -0,0 +1,54 @@
1
+ import importlib
2
+
3
+ import pytest
4
+
5
+ from openff.utilities.provenance import (
6
+ _get_conda_list_package_versions,
7
+ get_ambertools_version,
8
+ )
9
+ from openff.utilities.warnings import CondaExecutableNotFoundWarning
10
+
11
+
12
+ def mock_has_executable(executable_name: str) -> bool:
13
+ return False
14
+
15
+
16
+ @pytest.mark.leaky
17
+ def test_conda_unavailable_returns_empty_dict(monkeypatch):
18
+ """
19
+ Test that if `conda|etc. list` fails, with or without AmberTools (properly) installed,
20
+ get_ambertools_version returns `None`.
21
+
22
+ This test leaks! Do not run it alongside other tests! Select either it "pytest -m leaky"
23
+ or avoid it "pytest -m 'not leaky'"
24
+ """
25
+ with (
26
+ monkeypatch.context() as m,
27
+ pytest.warns(CondaExecutableNotFoundWarning, match="No .* executable found"),
28
+ ):
29
+ m.setenv("PIXI_IN_SHELL", "0")
30
+ m.setenv("CONDA_SHLVL", "0")
31
+
32
+ assert _get_conda_list_package_versions() == dict()
33
+
34
+
35
+ def test_conda_available_get_ambertools_version_found():
36
+ # Skip if ambertools is not installed - sander would a better import test
37
+ # for AmberTools (since parmed could come from the standalone
38
+ # parmed package), however it's currently broken on ARM macs.
39
+ pytest.importorskip("parmed")
40
+
41
+ assert get_ambertools_version() is not None, (
42
+ f"note that len of package versions is {len(_get_conda_list_package_versions())}"
43
+ )
44
+
45
+
46
+ def test_conda_available_get_ambertools_version_not_found():
47
+ try:
48
+ importlib.import_module("parmed")
49
+ except ImportError:
50
+ assert len(_get_conda_list_package_versions()) > 0
51
+ assert get_ambertools_version() is None
52
+ return
53
+
54
+ pytest.skip("only run when ambertools is not installed.")
@@ -0,0 +1 @@
1
+ from openff.utilities import * # noqa: F403
@@ -0,0 +1,16 @@
1
+ import pytest
2
+
3
+ from openff.utilities.testing import skip_if_missing, skip_if_missing_exec
4
+
5
+
6
+ def test_skips():
7
+ assert not skip_if_missing("pip").args[0]
8
+ assert skip_if_missing("numpynumpy").args[0]
9
+
10
+ assert not skip_if_missing_exec("python").args[0]
11
+ assert skip_if_missing_exec("python4").args[0]
12
+ assert not skip_if_missing_exec(["python", "python4"]).args[0]
13
+ assert skip_if_missing_exec(["python4", "python5"]).args[0]
14
+
15
+ with pytest.raises(ValueError, match="Bad type.*int"):
16
+ skip_if_missing_exec(0)
@@ -0,0 +1,181 @@
1
+ import os
2
+
3
+ import pytest
4
+
5
+ from openff.utilities.exceptions import MissingOptionalDependencyError
6
+ from openff.utilities.testing import skip_if_missing
7
+ from openff.utilities.utilities import (
8
+ get_data_dir_path,
9
+ get_data_file_path,
10
+ has_executable,
11
+ has_package,
12
+ requires_oe_module,
13
+ requires_package,
14
+ temporary_cd,
15
+ )
16
+
17
+
18
+ def compare_paths(path_1: str, path_2: str) -> bool:
19
+ """Checks whether two paths are the same.
20
+
21
+ Parameters
22
+ ----------
23
+ path_1
24
+ The first path.
25
+ path_2
26
+ The second path.
27
+
28
+ Returns
29
+ -------
30
+ True if the paths are equivalent.
31
+ """
32
+ return os.path.normpath(path_1) == os.path.normpath(path_2)
33
+
34
+
35
+ def test_get_data_dir_path():
36
+ """Tests that the `get_data_dir_path` can correctly find
37
+ data directories.
38
+ """
39
+
40
+ # Test a path which should exist.
41
+ data_file_path = get_data_dir_path("data/more", package_name="openff.utilities")
42
+ assert os.path.isdir(data_file_path)
43
+
44
+ # Ensure a double-checking through data/ takes place
45
+ data_file_path = get_data_dir_path("more", package_name="openff.utilities")
46
+ assert os.path.isdir(data_file_path)
47
+
48
+ # Test a path which should not exist.
49
+ with pytest.raises(NotADirectoryError):
50
+ get_data_dir_path("missing", package_name="openff.utilities")
51
+
52
+ # Test that a file directory raises NotaDirectoryError
53
+ with pytest.raises(NotADirectoryError):
54
+ get_data_dir_path("data/data.dat", package_name="openff.utilities")
55
+
56
+
57
+ def test_get_data_file_path():
58
+ """Tests that the `get_data_file_path` can correctly find
59
+ data files.
60
+ """
61
+
62
+ # Test a path which should exist.
63
+ data_file_path = get_data_file_path("data/data.dat", package_name="openff.utilities")
64
+ assert os.path.isfile(data_file_path)
65
+
66
+ # Ensure a double-checking through data/ takes place
67
+ data_file_path = get_data_file_path("data.dat", package_name="openff.utilities")
68
+ assert os.path.isfile(data_file_path)
69
+
70
+ # Test a path which should not exist.
71
+ with pytest.raises(FileNotFoundError):
72
+ get_data_file_path("missing.file", package_name="openff.utilities")
73
+
74
+ # Test that a directory raises FileNotFoundError
75
+ with pytest.raises(FileNotFoundError):
76
+ get_data_file_path("data/", package_name="openff.utilities")
77
+
78
+
79
+ def test_temporary_cd():
80
+ """Tests that temporary cd works as expected"""
81
+
82
+ original_directory = os.getcwd()
83
+
84
+ # Move to the parent directory
85
+ with temporary_cd(os.pardir):
86
+ current_directory = os.getcwd()
87
+ expected_directory = os.path.abspath(os.path.join(original_directory, os.pardir))
88
+
89
+ assert compare_paths(current_directory, expected_directory)
90
+
91
+ assert compare_paths(os.getcwd(), original_directory)
92
+
93
+ # Move to a temporary directory
94
+ with temporary_cd():
95
+ assert not compare_paths(os.getcwd(), original_directory)
96
+
97
+ assert compare_paths(os.getcwd(), original_directory)
98
+
99
+ # Move to the same directory
100
+ with temporary_cd(""):
101
+ assert compare_paths(os.getcwd(), original_directory)
102
+
103
+ assert compare_paths(os.getcwd(), original_directory)
104
+
105
+ with temporary_cd(os.curdir):
106
+ assert compare_paths(os.getcwd(), original_directory)
107
+
108
+ assert compare_paths(os.getcwd(), original_directory)
109
+
110
+
111
+ def test_has_package():
112
+ assert has_package("os")
113
+ assert has_package("pytest")
114
+ assert not has_package("nummmmmmpy")
115
+
116
+
117
+ def test_has_executable():
118
+ assert has_executable("pwd")
119
+ assert has_executable("pytest")
120
+
121
+ assert has_executable("/usr/bin/whoami")
122
+
123
+ assert not has_package("pyyyyython")
124
+
125
+
126
+ def test_requires_package():
127
+ """Tests that the ``requires_package`` utility behaves as expected."""
128
+
129
+ def dummy_function():
130
+ pass
131
+
132
+ # sys should always be found so this should not raise an exception.
133
+ requires_package("sys")(dummy_function)()
134
+
135
+ with pytest.raises(MissingOptionalDependencyError) as error_info:
136
+ requires_package("fake-lib")(dummy_function)()
137
+
138
+ assert error_info.value.library_name == "fake-lib"
139
+
140
+
141
+ @skip_if_missing("openeye.oechem")
142
+ @pytest.mark.skipif("OE_LICENSE" not in os.environ, reason="Requires an OpenEye license is NOT set up")
143
+ def test_requires_oe_module():
144
+ """Tests that the ``requires_package`` utility behaves as expected when an OpenEye license is set up."""
145
+
146
+ def dummy_function():
147
+ pass
148
+
149
+ requires_oe_module("oechem")(dummy_function)()
150
+
151
+
152
+ @pytest.mark.parametrize("oe_module", ["oechem", "oeiupac", "oeomega", "oequacpac", "oedepict"])
153
+ @skip_if_missing("openeye")
154
+ @pytest.mark.skipif("OE_LICENSE" in os.environ, reason="Requires an OpenEye license is NOT set up")
155
+ def test_requires_oe_module_installed_missing_license(oe_module):
156
+ """Tests that the ``requires_package`` utility behaves as expected while OpenEye toolkits are
157
+ installed but no OpenEye license is set up."""
158
+
159
+ def dummy_function():
160
+ pass
161
+
162
+ with pytest.raises(MissingOptionalDependencyError) as error_info:
163
+ requires_oe_module(oe_module)(dummy_function)()
164
+
165
+ assert oe_module in str(error_info.value)
166
+ assert "conda-forge" not in str(error_info.value)
167
+
168
+
169
+ @pytest.mark.skipif(has_package("openeye.oechem"), reason="Requires OpenEye toolkits are NOT installed")
170
+ def test_requires_oe_module_not_installed():
171
+ """Tests that the ``requires_package`` utility behaves as expected while OpenEye toolkits are
172
+ installed."""
173
+
174
+ def dummy_function():
175
+ pass
176
+
177
+ with pytest.raises(MissingOptionalDependencyError) as error_info:
178
+ requires_oe_module("oechem")(dummy_function)()
179
+
180
+ assert "oechem" in str(error_info.value)
181
+ assert "conda-forge" not in str(error_info.value)
@@ -0,0 +1,52 @@
1
+ class OpenFFError(BaseException):
2
+ """The base exception from which most custom exceptions in openff-utilities should inherit."""
3
+
4
+
5
+ class MissingOptionalDependencyError(OpenFFError, ImportError):
6
+ """An exception raised when an optional dependency is required
7
+ but cannot be found.
8
+
9
+ Attributes
10
+ ----------
11
+ library_name
12
+ The name of the missing library.
13
+ license_issue
14
+ Whether the library was importable but was unusable due
15
+ to a missing license.
16
+ """
17
+
18
+ def __init__(self, library_name: str, license_issue: bool = False):
19
+ """
20
+
21
+ Parameters
22
+ ----------
23
+ library_name
24
+ The name of the missing library.
25
+ license_issue
26
+ Whether the library was importable but was unusable due
27
+ to a missing license.
28
+ """
29
+
30
+ message = f"The required {library_name} module could not be imported."
31
+
32
+ if license_issue:
33
+ message = f"{message} This is due to a missing license."
34
+
35
+ library_name_corrected = library_name.replace(".", "-")
36
+
37
+ if "openeye" not in library_name:
38
+ message = (
39
+ f"{message} Try installing the package by running "
40
+ f"`conda install -c conda-forge {library_name_corrected}`"
41
+ )
42
+
43
+ super().__init__(message)
44
+
45
+ self.library_name = library_name
46
+ self.license_issue = license_issue
47
+
48
+
49
+ class CondaExecutableNotFoundError(OpenFFError):
50
+ """
51
+ A conda (or mamba/micromamba) executable is not found.
52
+ """
@@ -0,0 +1,66 @@
1
+ import functools
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import warnings
6
+ from typing import Optional
7
+
8
+
9
+ @functools.lru_cache
10
+ def _get_conda_list_package_versions() -> dict[str, str]:
11
+ """
12
+ Returns the versions of any packages found while executing `conda list`.
13
+ If no conda executable is found, emits CondaExecutableNotFoundWarning
14
+ """
15
+ from openff.utilities.warnings import CondaExecutableNotFoundWarning
16
+
17
+ if os.environ.get("PIXI_IN_SHELL") == "1" and os.environ.get("PIXI_EXE"):
18
+ conda_command = "{} list --json --manifest-path {}".format(
19
+ os.environ["PIXI_EXE"], os.environ["PIXI_PROJECT_MANIFEST"]
20
+ )
21
+ elif os.environ.get("CONDA_SHLVL", "0") != "0" and os.environ.get("CONDA_EXE"):
22
+ conda_command = "{} list --json".format(os.environ["CONDA_EXE"])
23
+ elif os.environ.get("CONDA_SHLVL", "0") != "0" and os.environ.get("MAMBA_EXE"):
24
+ conda_command = "{} list --json".format(os.environ["MAMBA_EXE"])
25
+ else:
26
+ warnings.warn(
27
+ "No conda/mamba/micromamba executable found. Unable to determine package versions.",
28
+ CondaExecutableNotFoundWarning,
29
+ )
30
+ return dict()
31
+
32
+ output = json.loads(subprocess.check_output(conda_command.split()).decode())
33
+
34
+ package_versions = {}
35
+ for _package in output:
36
+ package_versions[_package["name"]] = _package["version"]
37
+
38
+ return package_versions
39
+
40
+
41
+ def get_ambertools_version() -> Optional[str]:
42
+ """
43
+ Attempts to retrieve the version of the currently installed AmberTools.
44
+
45
+ There are two soft failure modes, each of which cause this function to return `None`:
46
+ 1. If there is a failure calling `{conda|mamba|etc.} list`, the user is
47
+ warned by `_get_conda_list_package_versions` and this function returns `None`.
48
+ 2. If there is a failure calling `{conda|mamba|etc.} list`, this function
49
+ still returns `None`, but without a warning associated with the above failure.
50
+ """
51
+
52
+ try:
53
+ return _get_conda_list_package_versions().get("ambertools", None)
54
+ except (
55
+ ValueError, # Issue 98
56
+ subprocess.CalledProcessError, # Issue 101
57
+ ):
58
+ from openff.utilities.warnings import CondaExecutableNotFoundWarning
59
+
60
+ warnings.warn(
61
+ "Something went wrong parsing the output of `conda list` or similar. Unable to "
62
+ "determine AmberTools version, returning None.",
63
+ CondaExecutableNotFoundWarning,
64
+ )
65
+
66
+ return None
@@ -0,0 +1,54 @@
1
+ from typing import TYPE_CHECKING, Optional, Union
2
+
3
+ from openff.utilities.utilities import has_executable, has_package
4
+
5
+ if TYPE_CHECKING:
6
+ from _pytest.mark.structures import MarkDecorator
7
+
8
+
9
+ def skip_if_missing(package_name: str, reason: Optional[str] = None) -> "MarkDecorator":
10
+ """
11
+ Helper function to generate a pytest.mark.skipif decorator
12
+ for any package. This allows tests to be skipped if some
13
+ optional dependency is not found.
14
+
15
+ Parameters
16
+ ----------
17
+ package_name : str
18
+ The name of the package that is required for a test(s)
19
+ reason : str, optional
20
+ Explanation of why the skipped it to be tested
21
+
22
+ Returns
23
+ -------
24
+ requires_package : _pytest.mark.structures.MarkDecorator
25
+ A pytest decorator that will skip tests if the package is not available
26
+ """
27
+ import pytest
28
+
29
+ if not reason:
30
+ reason = f"Package {package_name} is required, but was not found."
31
+ requires_package = pytest.mark.skipif(not has_package(package_name), reason=reason)
32
+ return requires_package
33
+
34
+
35
+ def skip_if_missing_exec(exec: Union[str, list[str]]) -> "MarkDecorator":
36
+ """Helper function to generate a pytest.mark.skipif decorator
37
+ if an executable(s) is not found."""
38
+ import pytest
39
+
40
+ execs: list[str]
41
+ if isinstance(exec, str):
42
+ execs = [exec]
43
+ elif isinstance(exec, list):
44
+ execs = exec
45
+ else:
46
+ raise ValueError(f"Bad type passed to skip_if_missing_exec. Found type {type(exec)}")
47
+
48
+ found_exec = False
49
+ for exec_ in execs:
50
+ found_exec = found_exec or has_executable(exec_)
51
+
52
+ reason = f"Package {exec!s} is required, but was not found."
53
+ mark = pytest.mark.skipif(not found_exec, reason=reason)
54
+ return mark
@@ -0,0 +1,261 @@
1
+ import errno
2
+ import importlib
3
+ import os
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+ from functools import wraps
7
+ from importlib.resources import as_file, files
8
+ from tempfile import TemporaryDirectory
9
+ from typing import Any, Callable, Literal, Optional, TypeVar
10
+
11
+ from openff.utilities.exceptions import MissingOptionalDependencyError
12
+
13
+ # https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
14
+
15
+ F = TypeVar("F", bound=Callable[..., Any])
16
+
17
+
18
+ def has_package(package_name: str) -> bool:
19
+ """
20
+ Helper function to generically check if a Python package is installed.
21
+ Intended to be used to check for optional dependencies.
22
+
23
+ Parameters
24
+ ----------
25
+ package_name : str
26
+ The name of the Python package to check the availability of
27
+
28
+ Returns
29
+ -------
30
+ package_available : bool
31
+ Boolean indicator if the package is available or not
32
+
33
+ Examples
34
+ --------
35
+ >>> has_numpy = has_package('numpy')
36
+ >>> has_numpy
37
+ True
38
+ >>> has_foo = has_package('other_non_installed_package')
39
+ >>> has_foo
40
+ False
41
+ """
42
+ try:
43
+ importlib.import_module(package_name)
44
+ except ModuleNotFoundError:
45
+ return False
46
+ return True
47
+
48
+
49
+ def requires_package(package_name: str) -> Callable[..., Any]:
50
+ """
51
+ Helper function to denote that a funciton requires some optional
52
+ dependency. A function decorated with this decorator will raise
53
+ `MissingOptionalDependencyError` if the package is not found by
54
+ `importlib.import_module()`.
55
+
56
+ Parameters
57
+ ----------
58
+ package_name : str
59
+ The name of the module to be imported.
60
+
61
+ Raises
62
+ ------
63
+ MissingOptionalDependencyError
64
+
65
+ """
66
+
67
+ def inner_decorator(function: F) -> F:
68
+ @wraps(function)
69
+ def wrapper(*args, **kwargs): # type: ignore[no-untyped-def]
70
+ import importlib
71
+
72
+ try:
73
+ importlib.import_module(package_name)
74
+ except ImportError:
75
+ raise MissingOptionalDependencyError(library_name=package_name)
76
+ except Exception as e:
77
+ raise e
78
+
79
+ return function(*args, **kwargs)
80
+
81
+ return wrapper # type: ignore[return-value]
82
+
83
+ return inner_decorator
84
+
85
+
86
+ def requires_oe_module(
87
+ module_name: Literal["oechem", "oeomega", "oequacpac", "oeiupac", "oedepict"],
88
+ ) -> Callable[..., Any]:
89
+ """
90
+ Helper function to denote that a funciton requires a particular OpenEye library.
91
+ A function decorated with this decorator will raise `MissingOptionalDependencyError` if
92
+ the module is not found by @requires_package or the module is not found to be
93
+ licensed.
94
+
95
+ Parameters
96
+ ----------
97
+ module_name : str
98
+ The name of the OpenEye module to be imported.
99
+
100
+ Raises
101
+ ------
102
+ MissingOptionalDependencyError
103
+ """
104
+
105
+ def inner_decorator(function: F) -> F:
106
+ @requires_package(f"openeye.{module_name}")
107
+ @wraps(function)
108
+ def wrapper(*args, **kwargs): # type: ignore[no-untyped-def]
109
+ oe_module = importlib.import_module(f"openeye.{module_name}")
110
+
111
+ license_functions = {
112
+ "oechem": "OEChemIsLicensed",
113
+ "oequacpac": "OEQuacPacIsLicensed",
114
+ "oeiupac": "OEIUPACIsLicensed",
115
+ "oeomega": "OEOmegaIsLicensed",
116
+ "oedepict": "OEDepictIsLicensed",
117
+ }
118
+
119
+ is_licensed = getattr(oe_module, license_functions[module_name])()
120
+
121
+ if not is_licensed:
122
+ raise MissingOptionalDependencyError(library_name=f"openeye.{module_name}", license_issue=True)
123
+
124
+ return function(*args, **kwargs)
125
+
126
+ return wrapper # type: ignore
127
+
128
+ return inner_decorator
129
+
130
+
131
+ def has_executable(program_name: str) -> bool:
132
+ import os
133
+
134
+ def _is_executable(fpath: str) -> bool:
135
+ return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
136
+
137
+ fpath, fname = os.path.split(program_name)
138
+ if fpath:
139
+ if _is_executable(program_name):
140
+ return True
141
+ else:
142
+ for path in os.environ["PATH"].split(os.pathsep):
143
+ path = path.strip('"')
144
+ exe_file = os.path.join(path, program_name)
145
+ if _is_executable(exe_file):
146
+ return True
147
+
148
+ return False
149
+
150
+
151
+ @contextmanager
152
+ def temporary_cd(directory_path: Optional[str] = None) -> Generator[None, None, None]:
153
+ """Temporarily move the current working directory to the path
154
+ specified. If no path is given, a temporary directory will be
155
+ created, moved into, and then destroyed when the context manager
156
+ is closed.
157
+
158
+ Parameters
159
+ ----------
160
+ directory_path: str, optional
161
+
162
+ Returns
163
+ -------
164
+
165
+ """
166
+
167
+ if directory_path is not None and len(directory_path) == 0:
168
+ yield
169
+ return
170
+
171
+ old_directory = os.getcwd()
172
+
173
+ try:
174
+ if directory_path is None:
175
+ with TemporaryDirectory() as new_directory:
176
+ os.chdir(new_directory)
177
+ yield
178
+
179
+ else:
180
+ os.chdir(directory_path)
181
+ yield
182
+
183
+ finally:
184
+ os.chdir(old_directory)
185
+
186
+
187
+ def get_data_dir_path(relative_path: str, package_name: str) -> str:
188
+ """Get the full path to a directory within a module's tree.
189
+
190
+ If no directory is found at `relative_path`, a second attempt will be made
191
+ with `data/` preprended. If no directory is found at path, a NotADirectoryError
192
+ is raised.
193
+
194
+ Parameters
195
+ ----------
196
+ relative_path : str
197
+ The relative path of the file to load.
198
+ package_name : str
199
+ The name of the package in which a file is to be loaded, i.e. "openff.toolkit" or "openff.evaluator".
200
+
201
+ Returns
202
+ -------
203
+ The absolute path to the file.
204
+
205
+ Raises
206
+ ------
207
+ NotADirectoryError
208
+
209
+ See Also
210
+ --------
211
+ get_data_file_path, for getting the path to a particular file in a data directory.
212
+
213
+ """
214
+ with as_file(files(package_name) / relative_path) as dir_path:
215
+ if dir_path.is_dir():
216
+ return dir_path.as_posix()
217
+
218
+ with as_file(files(package_name) / "data" / relative_path) as dir_path:
219
+ if dir_path.is_dir():
220
+ return dir_path.as_posix()
221
+
222
+ raise NotADirectoryError(f"Directory {relative_path} not found in {package_name}.")
223
+
224
+
225
+ def get_data_file_path(relative_path: str, package_name: str) -> str:
226
+ """Get the full path to one of the files in the data directory.
227
+
228
+ If no file is found at `relative_path`, a second attempt will be made
229
+ with `data/` preprended. If no files exist at either path, a FileNotFoundError
230
+ is raised.
231
+
232
+ Parameters
233
+ ----------
234
+ relative_path : str
235
+ The relative path of the file to load.
236
+ package_name : str
237
+ The name of the package in which a file is to be loaded, i.e.
238
+ "openff.toolkit" or "openff.evaluator"
239
+
240
+ Returns
241
+ -------
242
+ The absolute path to the file.
243
+
244
+ Raises
245
+ ------
246
+ FileNotFoundError
247
+
248
+ See Also
249
+ --------
250
+ get_data_dir_path, for getting the path to a directory instead of an individual file.
251
+
252
+ """
253
+ with as_file(files(package_name) / relative_path) as file_path:
254
+ if file_path.is_file():
255
+ return file_path.as_posix()
256
+
257
+ with as_file(files(package_name) / "data" / relative_path) as file_path:
258
+ if file_path.is_file():
259
+ return file_path.as_posix()
260
+
261
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), file_path)
@@ -0,0 +1,4 @@
1
+ class CondaExecutableNotFoundWarning(UserWarning):
2
+ """
3
+ A conda (or mamba/micromamba) executable is not found.
4
+ """
@@ -0,0 +1,85 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools",
4
+ "versioningit",
5
+ ]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [project]
9
+ name = "acellera-openff-utilities"
10
+ description = "A collection of miscellaneous utility functions used throughout the OpenFF stack"
11
+ readme = "README.md"
12
+ authors = [{name = "Open Force Field Initiative", email = "info@openforcefield.org"}]
13
+ license = {text = "MIT"}
14
+ dynamic = ["version"]
15
+
16
+ [tool.setuptools.packages]
17
+ find = {}
18
+
19
+ [tool.versioningit]
20
+
21
+ [tool.ruff]
22
+ line-length = 119
23
+ namespace-packages = ["openff/utilities/"]
24
+
25
+ [tool.ruff.lint]
26
+ select = ["F", "E", "W", "NPY", "UP", "RUF", "I"]
27
+
28
+ [tool.ruff.lint.per-file-ignores]
29
+ "openff/utilities/__init__.py" = ["F401"]
30
+
31
+ [tool.mypy]
32
+ exclude="openff/utilities/_tests"
33
+ python_version = "3.12"
34
+ warn_unused_configs = true
35
+ warn_unused_ignores = true
36
+ show_error_codes = true
37
+ strict = true
38
+
39
+ [tool.pytest.ini_options]
40
+ markers = [
41
+ "leaky: run leaky tests",
42
+ ]
43
+ addopts = "--cov=openff/utilities --cov-report=xml -m \"not leaky\""
44
+
45
+ [tool.coverage.run]
46
+ omit = [
47
+ "*/*/_tests/*",
48
+ ]
49
+
50
+ [tool.coverage.report]
51
+ exclude_lines = [
52
+ "pragma: no cover",
53
+ "if TYPE_CHECKING:",
54
+ "raise NotImplementedError",
55
+ "@overload",
56
+ ]
57
+
58
+ [tool.pixi.project]
59
+ channels = ["conda-forge", "openeye"]
60
+ platforms = ["linux-64", "osx-64", "osx-arm64"]
61
+
62
+ [tool.pixi.dependencies]
63
+ python = "*"
64
+ pip = "*"
65
+ versioningit = "*"
66
+ pytest = "*"
67
+ pytest-cov = "*"
68
+ pytest-xdist = "*"
69
+ pytest-randomly = "*"
70
+ nbval = "*"
71
+ mypy = "*"
72
+ types-setuptools = "*"
73
+ types-toml = "*"
74
+
75
+ [tool.pixi.pypi-dependencies]
76
+ openff-utilities = { path = ".", editable = true }
77
+
78
+ [tool.pixi.feature.py311.dependencies]
79
+ python = "3.11.*"
80
+ [tool.pixi.feature.py312.dependencies]
81
+ python = "3.12.*"
82
+
83
+ [tool.pixi.environments]
84
+ py311 = ["py311"]
85
+ py312 = ["py312"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+