Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-35901: Add new test case for importing every python file in a package #165

Merged
merged 5 commits into from
Jul 7, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -96,7 +96,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: "3.10"

- name: Install dependencies
run: |
Expand Down
3 changes: 3 additions & 0 deletions doc/changes/DM-35901.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added ``lsst.utils.tests.ImportTestCase`` class.
This test case can be used to force the import of every file in a package.
This can be very useful for spotting obvious problems if a package does not export every file by default.
1 change: 1 addition & 0 deletions doc/changes/DM-35901.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dropped support for Python 3.8.
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand Down
2 changes: 1 addition & 1 deletion python/lsst/utils/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ def write(self, filename: str) -> None:
ff.write(self.toBytes(self.formats[ext]))

def __str__(self) -> str:
ss = "%s({\n" % self.__class__.__name__
ss = self.__class__.__name__ + "({\n"
# Sort alphabetically by module name, for convenience in reading
ss += ",\n".join(f"{prod!r}:{self[prod]!r}" for prod in sorted(self))
ss += ",\n})"
Expand Down
82 changes: 72 additions & 10 deletions python/lsst/utils/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"init",
"MemoryTestCase",
"ExecutablesTestCase",
"ImportTestCase",
"getTempFilePath",
"TestCase",
"assertFloatsAlmostEqual",
Expand All @@ -31,6 +32,7 @@
import contextlib
import functools
import gc
import importlib.resources as resources
import inspect
import itertools
import os
Expand All @@ -41,12 +43,14 @@
import tempfile
import unittest
import warnings
from collections.abc import Callable, Iterator, Mapping, Sequence
from typing import Any
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from typing import Any, ClassVar

import numpy
import psutil

from .doImport import doImport

# Initialize the list of open files to an empty set
open_files = set()

Expand Down Expand Up @@ -160,7 +164,7 @@
diff = now_open.difference(open_files)
if diff:
for f in diff:
print("File open: %s" % f)
print(f"File open: {f}")

Check warning on line 167 in python/lsst/utils/tests.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/utils/tests.py#L167

Added line #L167 was not covered by tests
self.fail("Failed to close %d file%s" % (len(diff), "s" if len(diff) != 1 else ""))


Expand Down Expand Up @@ -331,6 +335,67 @@
cls._build_test_method(e, ref_dir)


class ImportTestCase(unittest.TestCase):
"""Test that the named packages can be imported and all files within
that package.

The test methods are created dynamically. Callers must subclass this
method and define the ``PACKAGES`` property.
"""

PACKAGES: ClassVar[Iterable[str]] = ()
"""Packages to be imported."""

_n_registered = 0
"""Number of packages registered for testing by this class."""

def _test_no_packages_registered_for_import_testing(self) -> None:
"""Test when no packages have been registered.

Without this, if no packages have been listed no tests will be
registered and the test system will not report on anything. This
test fails and reports why.
"""
raise AssertionError("No packages registered with import test. Was the PACKAGES property set?")

def __init_subclass__(cls, **kwargs: Any) -> None:
"""Create the test methods based on the content of the ``PACKAGES``
class property.
"""
super().__init_subclass__(**kwargs)

for mod in cls.PACKAGES:
test_name = "test_import_" + mod.replace(".", "_")

def test_import(*args: Any) -> None:
self = args[0]
self.assertImport(mod)

test_import.__name__ = test_name
setattr(cls, test_name, test_import)
cls._n_registered += 1

# If there are no packages listed that is likely a mistake and
# so register a failing test.
if cls._n_registered == 0:
setattr(cls, "test_no_packages_registered", cls._test_no_packages_registered_for_import_testing)

Check warning on line 381 in python/lsst/utils/tests.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/utils/tests.py#L381

Added line #L381 was not covered by tests

def assertImport(self, root_pkg):
for file in resources.files(root_pkg).iterdir():
file = file.name
if not file.endswith(".py"):
continue
if file.startswith("__"):
continue
root, _ = os.path.splitext(file)
module_name = f"{root_pkg}.{root}"
with self.subTest(module=module_name):
try:
doImport(module_name)
except ImportError as e:

Check warning on line 395 in python/lsst/utils/tests.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/utils/tests.py#L395

Added line #L395 was not covered by tests
raise AssertionError(f"Error importing module {module_name}: {e}") from e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to test the failing case? Maybe by writing out a junk python file to a temp directory?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure how I would catch the failure since I'm not writing the test that is failing, the infrastructure is writing it and attaching it directly to this class.



@contextlib.contextmanager
def getTempFilePath(ext: str, expectOutput: bool = True) -> Iterator[str]:
"""Return a path suitable for a temporary file and try to delete the
Expand Down Expand Up @@ -687,17 +752,14 @@
if rtol is None:
errMsg = [f"{lhs} {cmpStr} {rhs}; diff={absDiff} with atol={atol}"]
elif atol is None:
errMsg = [
"%s %s %s; diff=%s/%s=%s with rtol=%s"
% (lhs, cmpStr, rhs, absDiff, relTo, absDiff / relTo, rtol)
]
errMsg = [f"{lhs} {cmpStr} {rhs}; diff={absDiff}/{relTo}={absDiff / relTo} with rtol={rtol}"]
else:
errMsg = [
"%s %s %s; diff=%s/%s=%s with rtol=%s, atol=%s"
% (lhs, cmpStr, rhs, absDiff, relTo, absDiff / relTo, rtol, atol)
f"{lhs} {cmpStr} {rhs}; diff={absDiff}/{relTo}={absDiff / relTo} "
f"with rtol={rtol}, atol={atol}"
]
else:
errMsg = ["%d/%d elements %s with rtol=%s, atol=%s" % (bad.sum(), bad.size, failStr, rtol, atol)]
errMsg = [f"{bad.sum()}/{bad.size} elements {failStr} with rtol={rtol}, atol={atol}"]
if plotOnFailure:
if len(lhs.shape) != 2 or len(rhs.shape) != 2:
raise ValueError("plotOnFailure is only valid for 2-d arrays")
Expand Down
32 changes: 32 additions & 0 deletions tests/test_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# This file is part of utils.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (http://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# Use of this source code is governed by a 3-clause BSD-style
# license that can be found in the LICENSE file.

import unittest

from lsst.utils.tests import ImportTestCase


class TestImports(ImportTestCase):
"""Test we can run the import tests.

All the code in utils is being tested and imported already but this
will confirm the test infrastructure works.
"""

PACKAGES = ("lsst.utils",)

def test_counter(self):
"""Test that the expected number of packages were registered."""
self.assertEqual(self._n_registered, 1)


if __name__ == "__main__":
unittest.main()