Skip to content

Commit 74747a8

Browse files
committed
Added new "import_tools" submodule.
1 parent 1f46d2e commit 74747a8

File tree

6 files changed

+199
-5
lines changed

6 files changed

+199
-5
lines changed

domdf_python_tools/import_tools.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python
2+
#
3+
# import_tools.py
4+
"""
5+
Functions for importing classes.
6+
"""
7+
#
8+
# Copyright © 2020 Dominic Davis-Foster <[email protected]>
9+
#
10+
# This program is free software; you can redistribute it and/or modify
11+
# it under the terms of the GNU Lesser General Public License as published by
12+
# the Free Software Foundation; either version 3 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU Lesser General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU Lesser General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
23+
# MA 02110-1301, USA.
24+
#
25+
# Based on https://github.com/asottile/git-code-debt/blob/master/git_code_debt/util/discovery.py
26+
# Copyright (c) 2014 Anthony Sottile
27+
# Licensed under the MIT License
28+
# | Permission is hereby granted, free of charge, to any person obtaining a copy
29+
# | of this software and associated documentation files (the "Software"), to deal
30+
# | in the Software without restriction, including without limitation the rights
31+
# | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32+
# | copies of the Software, and to permit persons to whom the Software is
33+
# | furnished to do so, subject to the following conditions:
34+
# |
35+
# | The above copyright notice and this permission notice shall be included in
36+
# | all copies or substantial portions of the Software.
37+
# |
38+
# | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39+
# | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40+
# | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41+
# | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42+
# | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43+
# | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
44+
# | THE SOFTWARE.
45+
#
46+
47+
# stdlib
48+
import inspect
49+
import pkgutil
50+
from types import ModuleType
51+
from typing import Any, Callable, List, Optional, Type
52+
53+
54+
def discover(
55+
package: ModuleType,
56+
match_func: Optional[Callable[[Any], bool]] = None,
57+
) -> List[Type[Any]]:
58+
"""
59+
Returns a set of objects in the directory matched by match_func
60+
61+
:param package: A Python package
62+
:param match_func: Function taking an object and returning true if the object is to be included in the output.
63+
64+
:return:
65+
:rtype:
66+
"""
67+
68+
matched_classes = list()
69+
70+
for _, module_name, _ in pkgutil.walk_packages(
71+
# https://github.com/python/mypy/issues/1422
72+
# Stalled PRs: https://github.com/python/mypy/pull/3527
73+
# https://github.com/python/mypy/pull/5212
74+
package.__path__, # type: ignore
75+
prefix=package.__name__ + '.',
76+
):
77+
module = __import__(module_name, fromlist=['__trash'], level=0)
78+
79+
# Check all the functions in that module
80+
for _, imported_objects in inspect.getmembers(module, match_func):
81+
if not hasattr(imported_objects, "__module__"):
82+
continue
83+
84+
# Don't include things that are only there due to a side effect of importing
85+
if imported_objects.__module__ != module.__name__:
86+
continue
87+
88+
matched_classes.append(imported_objects)
89+
90+
return matched_classes

domdf_python_tools/versions.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def __eq__(self, other) -> bool:
9999
other = _prep_for_eq(other)
100100

101101
if other is NotImplemented:
102-
return NotImplemented
102+
return NotImplemented # pragma: no cover
103103
else:
104104
shortest = min(len(self), (len(other)))
105105
return self[:shortest] == other[:shortest]
@@ -114,7 +114,7 @@ def __gt__(self, other) -> bool:
114114
other = _prep_for_eq(other)
115115

116116
if other is NotImplemented:
117-
return NotImplemented
117+
return NotImplemented # pragma: no cover
118118
else:
119119
return tuple(self) > other
120120

@@ -128,7 +128,7 @@ def __lt__(self, other) -> bool:
128128
other = _prep_for_eq(other)
129129

130130
if other is NotImplemented:
131-
return NotImplemented
131+
return NotImplemented # pragma: no cover
132132
else:
133133
return tuple(self) < other
134134

@@ -142,7 +142,7 @@ def __ge__(self, other) -> bool:
142142
other = _prep_for_eq(other)
143143

144144
if other is NotImplemented:
145-
return NotImplemented
145+
return NotImplemented # pragma: no cover
146146
else:
147147
return tuple(self)[:len(other)] >= other
148148

@@ -156,7 +156,7 @@ def __le__(self, other) -> bool:
156156
other = _prep_for_eq(other)
157157

158158
if other is NotImplemented:
159-
return NotImplemented
159+
return NotImplemented # pragma: no cover
160160
else:
161161
return tuple(self)[:len(other)] <= other
162162

tests/discover_demo_module/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def foo():
2+
pass
3+
4+
5+
def bar():
6+
pass
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from string import ascii_letters
2+
from math import ceil
3+
4+
class Bob():
5+
pass
6+
7+
8+
class Alice():
9+
pass

tests/test_import_tools.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# stdlib
2+
import inspect
3+
import sys
4+
from contextlib import contextmanager
5+
6+
# 3rd party
7+
import pytest
8+
9+
# this package
10+
from domdf_python_tools.import_tools import discover
11+
12+
sys.path.append(".")
13+
sys.path.append("tests")
14+
15+
# 3rd partys
16+
import discover_demo_module
17+
18+
19+
def test_discover():
20+
# Alphabetical order regardless of order in the module.
21+
assert discover(discover_demo_module) == [
22+
discover_demo_module.submodule_a.bar,
23+
discover_demo_module.submodule_a.foo,
24+
discover_demo_module.submodule_b.Alice,
25+
discover_demo_module.submodule_b.Bob,
26+
]
27+
28+
29+
def test_discover_function_only():
30+
# Alphabetical order regardless of order in the module.
31+
assert discover(discover_demo_module, match_func=inspect.isfunction) == [
32+
discover_demo_module.submodule_a.bar,
33+
discover_demo_module.submodule_a.foo,
34+
]
35+
36+
37+
def test_discover_class_only():
38+
# Alphabetical order regardless of order in the module.
39+
assert discover(discover_demo_module, match_func=inspect.isclass) == [
40+
discover_demo_module.submodule_b.Alice,
41+
discover_demo_module.submodule_b.Bob,
42+
]
43+
44+
45+
def test_discover_hasattr():
46+
47+
def match_func(obj):
48+
return hasattr(obj, "foo")
49+
50+
assert discover(discover_demo_module, match_func=match_func) == []
51+
52+
53+
class HasPath:
54+
55+
__path__ = "foo"
56+
57+
58+
@contextmanager
59+
def does_not_raise():
60+
yield
61+
62+
63+
if sys.version_info <= (3, 7):
64+
haspath_error = does_not_raise()
65+
else:
66+
haspath_error = pytest.raises(ValueError, match="^path must be None or list of paths to look for modules in$")
67+
68+
69+
def raises_attribute_error(obj, **kwargs):
70+
return pytest.param(
71+
obj,
72+
pytest.raises(AttributeError, match=f"^'{type(obj).__name__}' object has no attribute '__path__'$"),
73+
**kwargs,
74+
)
75+
76+
77+
@pytest.mark.parametrize("obj, expects", [
78+
raises_attribute_error("abc", id="string"),
79+
raises_attribute_error(123, id="int"),
80+
raises_attribute_error(12.34, id="float"),
81+
raises_attribute_error([1, 2, 3], id="list"),
82+
raises_attribute_error((1, 2, 3), id="tuple"),
83+
raises_attribute_error({1, 2, 3}, id="set"),
84+
raises_attribute_error({"a": 1, "b": 2, "c": 3}, id="dictionary"),
85+
pytest.param(HasPath, haspath_error, id="HasPath"),
86+
])
87+
def test_discover_errors(obj, expects):
88+
with expects:
89+
discover(obj)

0 commit comments

Comments
 (0)