Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install testtools
python -m pip install ruff
- name: Test with testtools
run: |
python -m testtools.run extras.tests.test_suite
- name: Lint with ruff
run: |
python -m ruff check .
17 changes: 9 additions & 8 deletions extras/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import sys

__all__ = [
'try_import',
'try_imports',
]
"try_import",
"try_imports",
]

# same format as sys.version_info: "A tuple containing the five components of
# the version number: major, minor, micro, releaselevel, and serial. All
Expand All @@ -21,7 +21,7 @@
# If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno).

__version__ = (1, 0, 0, 'final', 0)
__version__ = (1, 0, 0, "final", 0)


def try_import(name, alternative=None, error_callback=None):
Expand All @@ -37,14 +37,14 @@ def try_import(name, alternative=None, error_callback=None):
:param error_callback: If non-None, a callable that is passed the ImportError
when the module cannot be loaded.
"""
module_segments = name.split('.')
module_segments = name.split(".")
last_error = None
remainder = []
# module_name will be what successfully imports. We cannot walk from the
# __import__ result because in import loops (A imports A.B, which imports
# C, which calls try_import("A.B")) A.B will not yet be set.
while module_segments:
module_name = '.'.join(module_segments)
module_name = ".".join(module_segments)
try:
__import__(module_name)
except ImportError:
Expand All @@ -69,6 +69,8 @@ def try_import(name, alternative=None, error_callback=None):


_RAISE_EXCEPTION = object()


def try_imports(module_names, alternative=_RAISE_EXCEPTION, error_callback=None):
"""Attempt to import modules.

Expand All @@ -92,6 +94,5 @@ def try_imports(module_names, alternative=_RAISE_EXCEPTION, error_callback=None)
if module:
return module
if alternative is _RAISE_EXCEPTION:
raise ImportError(
"Could not import any of: %s" % ', '.join(module_names))
raise ImportError("Could not import any of: %s" % ", ".join(module_names))
return alternative
5 changes: 3 additions & 2 deletions extras/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
def test_suite():
from extras.tests import (
test_extras,
)
)

modules = [
test_extras,
]
]
loader = TestLoader()
suites = map(loader.loadTestsFromModule, modules)
return TestSuite(suites)
74 changes: 38 additions & 36 deletions extras/tests/test_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
Equals,
Is,
Not,
)
)

from extras import (
try_import,
try_imports,
)
)

def check_error_callback(test, function, arg, expected_error_count,
expect_result):

def check_error_callback(test, function, arg, expected_error_count, expect_result):
"""General test template for error_callback argument.

:param test: Test case instance.
Expand All @@ -27,9 +27,11 @@ def check_error_callback(test, function, arg, expected_error_count,
ultimately be returned or not.
"""
cb_calls = []

def cb(e):
test.assertIsInstance(e, ImportError)
cb_calls.append(e)

try:
result = function(arg, error_callback=cb)
except ImportError:
Expand All @@ -43,58 +45,60 @@ def cb(e):


class TestTryImport(TestCase):

def test_doesnt_exist(self):
# try_import('thing', foo) returns foo if 'thing' doesn't exist.
marker = object()
result = try_import('doesntexist', marker)
result = try_import("doesntexist", marker)
self.assertThat(result, Is(marker))

def test_None_is_default_alternative(self):
# try_import('thing') returns None if 'thing' doesn't exist.
result = try_import('doesntexist')
result = try_import("doesntexist")
self.assertThat(result, Is(None))

def test_existing_module(self):
# try_import('thing', foo) imports 'thing' and returns it if it's a
# module that exists.
result = try_import('os', object())
result = try_import("os", object())
import os

self.assertThat(result, Is(os))

def test_existing_submodule(self):
# try_import('thing.another', foo) imports 'thing' and returns it if
# it's a module that exists.
result = try_import('os.path', object())
result = try_import("os.path", object())
import os

self.assertThat(result, Is(os.path))

def test_nonexistent_submodule(self):
# try_import('thing.another', foo) imports 'thing' and returns foo if
# 'another' doesn't exist.
marker = object()
result = try_import('os.doesntexist', marker)
result = try_import("os.doesntexist", marker)
self.assertThat(result, Is(marker))

def test_object_from_module(self):
# try_import('thing.object') imports 'thing' and returns
# 'thing.object' if 'thing' is a module and 'object' is not.
result = try_import('os.path.join')
result = try_import("os.path.join")
import os

self.assertThat(result, Is(os.path.join))

def test_error_callback(self):
# the error callback is called on failures.
check_error_callback(self, try_import, 'doesntexist', 1, False)
check_error_callback(self, try_import, "doesntexist", 1, False)

def test_error_callback_missing_module_member(self):
# the error callback is called on failures to find an object
# inside an existing module.
check_error_callback(self, try_import, 'os.nonexistent', 1, False)
check_error_callback(self, try_import, "os.nonexistent", 1, False)

def test_error_callback_not_on_success(self):
# the error callback is not called on success.
check_error_callback(self, try_import, 'os.path', 0, True)
check_error_callback(self, try_import, "os.path", 0, True)

def test_handle_partly_imported_name(self):
# try_import('thing.other') when thing.other is mid-import
Expand All @@ -114,62 +118,60 @@ def test_handle_partly_imported_name(self):


class TestTryImports(TestCase):

def test_doesnt_exist(self):
# try_imports('thing', foo) returns foo if 'thing' doesn't exist.
marker = object()
result = try_imports(['doesntexist'], marker)
result = try_imports(["doesntexist"], marker)
self.assertThat(result, Is(marker))

def test_fallback(self):
result = try_imports(['doesntexist', 'os'])
result = try_imports(["doesntexist", "os"])
import os

self.assertThat(result, Is(os))

def test_None_is_default_alternative(self):
# try_imports('thing') returns None if 'thing' doesn't exist.
e = self.assertRaises(
ImportError, try_imports, ['doesntexist', 'noreally'])
e = self.assertRaises(ImportError, try_imports, ["doesntexist", "noreally"])
self.assertThat(
str(e),
Equals("Could not import any of: doesntexist, noreally"))
str(e), Equals("Could not import any of: doesntexist, noreally")
)

def test_existing_module(self):
# try_imports('thing', foo) imports 'thing' and returns it if it's a
# module that exists.
result = try_imports(['os'], object())
result = try_imports(["os"], object())
import os

self.assertThat(result, Is(os))

def test_existing_submodule(self):
# try_imports('thing.another', foo) imports 'thing' and returns it if
# it's a module that exists.
result = try_imports(['os.path'], object())
result = try_imports(["os.path"], object())
import os

self.assertThat(result, Is(os.path))

def test_nonexistent_submodule(self):
# try_imports('thing.another', foo) imports 'thing' and returns foo if
# 'another' doesn't exist.
marker = object()
result = try_imports(['os.doesntexist'], marker)
result = try_imports(["os.doesntexist"], marker)
self.assertThat(result, Is(marker))

def test_fallback_submodule(self):
result = try_imports(['os.doesntexist', 'os.path'])
result = try_imports(["os.doesntexist", "os.path"])
import os

self.assertThat(result, Is(os.path))

def test_error_callback(self):
# One error for every class that doesn't exist.
check_error_callback(self, try_imports,
['os.doesntexist', 'os.notthiseither'],
2, False)
check_error_callback(self, try_imports,
['os.doesntexist', 'os.notthiseither', 'os'],
2, True)
check_error_callback(self, try_imports,
['os.path'],
0, True)


check_error_callback(
self, try_imports, ["os.doesntexist", "os.notthiseither"], 2, False
)
check_error_callback(
self, try_imports, ["os.doesntexist", "os.notthiseither", "os"], 2, True
)
check_error_callback(self, try_imports, ["os.path"], 0, True)
48 changes: 25 additions & 23 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,38 @@
import os.path

import extras
testtools_cmd = extras.try_import('testtools.TestCommand')

testtools_cmd = extras.try_import("testtools.TestCommand")


def get_version():
"""Return the version of extras that we are building."""
version = '.'.join(
str(component) for component in extras.__version__[0:3])
version = ".".join(str(component) for component in extras.__version__[0:3])
return version


def get_long_description():
readme_path = os.path.join(
os.path.dirname(__file__), 'README.rst')
readme_path = os.path.join(os.path.dirname(__file__), "README.rst")
return open(readme_path).read()


cmdclass = {}

if testtools_cmd is not None:
cmdclass['test'] = testtools_cmd


setup(name='extras',
author='Testing cabal',
author_email='testtools-dev@lists.launchpad.net',
url='https://github.com/testing-cabal/extras',
description=('Useful extra bits for Python - things that should be '
'in the standard library'),
long_description=get_long_description(),
version=get_version(),
classifiers=[
cmdclass["test"] = testtools_cmd


setup(
name="extras",
author="Testing cabal",
author_email="testtools-dev@lists.launchpad.net",
url="https://github.com/testing-cabal/extras",
description=(
"Useful extra bits for Python - things that should be in the standard library"
),
long_description=get_long_description(),
version=get_version(),
classifiers=[
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
Expand All @@ -48,9 +49,10 @@ def get_long_description():
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
packages=[
'extras',
'extras.tests',
],
cmdclass=cmdclass)
],
packages=[
"extras",
"extras.tests",
],
cmdclass=cmdclass,
)
Loading