diff --git a/.github/workflows/py33.yml b/.github/workflows/py33.yml deleted file mode 100644 index 57211f6..0000000 --- a/.github/workflows/py33.yml +++ /dev/null @@ -1,22 +0,0 @@ -on: - push: - pull_request: - -jobs: - oldpython3: - # ubuntu-20+ doesnt support Python 3.3 or 3.4 - runs-on: ubuntu-18.04 - strategy: - matrix: - python-version: [3.3, 3.4] - name: oldpython3 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - run: python -m pip install tox==2.9.1 virtualenv==15.2.0 - - run: tox -e py33 - if: ${{ matrix.python-version == '3.3' }} - - run: tox -e py34 - if: ${{ matrix.python-version == '3.4' }} diff --git a/.github/workflows/tox-gh.yml b/.github/workflows/tox-gh.yml index 110b529..06abbe2 100644 --- a/.github/workflows/tox-gh.yml +++ b/.github/workflows/tox-gh.yml @@ -4,14 +4,13 @@ on: jobs: build: - # ubuntu-22 doesnt support Python 2.7 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10'] + python-version: [3.8, 3.9, '3.10'] name: main steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} diff --git a/nose/plugins/attrib.py b/nose/plugins/attrib.py index 59db28a..2893c1c 100644 --- a/nose/plugins/attrib.py +++ b/nose/plugins/attrib.py @@ -108,7 +108,6 @@ def test_end_to_end_something(self): from nose.plugins.base import Plugin from nose.util import tolist -log = logging.getLogger('nose.plugins.attrib') compat_24 = sys.version_info >= (2, 4) def attr(*args, **kwargs): @@ -124,7 +123,7 @@ def wrap_ob(ob): return wrap_ob def get_method_attr(method, cls, attr_name, default = False): - """Look up an attribute on a method/ function. + """Look up an attribute on a method/ function. If the attribute isn't found there, looking it up in the method's class, if any. """ @@ -139,7 +138,7 @@ def get_method_attr(method, cls, attr_name, default = False): class ContextHelper: """Object that can act as context dictionary for eval and looks up - names as attributes on a method/ function and its class. + names as attributes on a method/ function and its class. """ def __init__(self, method, cls): self.method = method @@ -156,6 +155,7 @@ class AttributeSelector(Plugin): def __init__(self): Plugin.__init__(self) self.attribs = [] + self.verbose_skipped = False def options(self, parser, env): """Register command line options""" @@ -165,6 +165,12 @@ def options(self, parser, env): metavar="ATTR", help="Run only tests that have attributes " "specified by ATTR [NOSE_ATTR]") + parser.add_option("--verbose-skipped-attr", + dest="verbose_skipped_attr", + default=False, + action="store_true", + help="Show debug messages when the test case is skipped") + # disable in < 2.4: eval can't take needed args if compat_24: parser.add_option("-A", "--eval-attr", @@ -184,6 +190,7 @@ def configure(self, options, config): match. """ self.attribs = [] + self.logger = logging.getLogger('nose.plugins.attrib') # handle python eval-expression parameter if compat_24 and options.eval_attr: @@ -227,6 +234,11 @@ def eval_in_context(expr, obj, cls): if self.attribs: self.enabled = True + self.verbose_skipped = options.verbose_skipped_attr + if self.verbose_skipped: + self.logger.setLevel(logging.DEBUG) + self.logger.debug("Show skipped attr tests later ...") + def validateAttrib(self, method, cls = None): """Verify whether a method has the required attributes The method is considered a match if it matches all attributes @@ -274,7 +286,10 @@ def validateAttrib(self, method, cls = None): def wantFunction(self, function): """Accept the function if its attributes match. """ - return self.validateAttrib(function) + is_valid = self.validateAttrib(function) + if self.verbose_skipped and not is_valid: + self.logger.debug("Skip the test: %s", function.__name__) + return is_valid def wantMethod(self, method): """Accept the method if its attributes match. @@ -286,4 +301,7 @@ def wantMethod(self, method): cls = method.__self__.__class__ except AttributeError: return False - return self.validateAttrib(method, cls) + is_valid = self.validateAttrib(method, cls) + if self.verbose_skipped and not is_valid: + self.logger.debug("Skip the test method: %s", method.__name__) + return is_valid diff --git a/unit_tests/test_plugins.py b/unit_tests/test_plugins.py index a4c3143..6d6510a 100644 --- a/unit_tests/test_plugins.py +++ b/unit_tests/test_plugins.py @@ -20,18 +20,18 @@ from mock import * class P(Plugin): - """Plugin of destiny!""" + """Plugin of destiny!""" pass class ErrPlugin(object): def load(self): raise Exception("Failed to load the plugin") - + class ErrPkgResources(object): def iter_entry_points(self, ep): yield ErrPlugin() - + # some plugins have 2.4-only features compat_24 = sys.version_info >= (2, 4) @@ -43,7 +43,7 @@ def setUp(self): def tearDown(self): sys.path = self.p[:] - + def test_add_options(self): conf = Config() opt = Bucket() @@ -63,7 +63,7 @@ def test_add_options(self): plug.configure(opt, conf) assert plug.enabled - + class TestDoctestPlugin(unittest.TestCase): def setUp(self): @@ -71,21 +71,21 @@ def setUp(self): def tearDown(self): sys.path = self.p[:] - + def test_add_options(self): # doctest plugin adds some options... conf = Config() opt = Bucket() parser = MockOptParser() plug = Doctest() - + plug.add_options(parser, {}) o, d = parser.opts[0] assert o[0] == '--with-doctest' o2, d2 = parser.opts[1] assert o2[0] == '--doctest-tests' - + o3, d3 = parser.opts[2] assert o3[0] == '--doctest-extension' @@ -98,7 +98,7 @@ def test_config(self): dtp = Doctest() dtp.add_options(parser, env) options, args = parser.parse_args(argv) - + print(options) print(args) self.assertEqual(options.doctestExtension, ['ext', 'txt']) @@ -110,7 +110,7 @@ def test_config(self): print(options) print(args) self.assertEqual(options.doctestExtension, ['txt']) - + def test_want_file(self): # doctest plugin can select module and/or non-module files conf = Config() @@ -118,18 +118,18 @@ def test_want_file(self): plug = Doctest() plug.can_configure = True plug.configure(opt, conf) - + assert plug.wantFile('foo.py') assert not plug.wantFile('bar.txt') assert not plug.wantFile('buz.rst') assert not plug.wantFile('bing.mov') - + plug.extension = ['.txt', '.rst'] assert plug.wantFile('/path/to/foo.py') assert plug.wantFile('/path/to/bar.txt') assert plug.wantFile('/path/to/buz.rst') assert not plug.wantFile('/path/to/bing.mov') - + def test_matches(self): # doctest plugin wants tests from all NON-test modules conf = Config() @@ -146,13 +146,13 @@ def test_collect_pymodule(self): if not support in sys.path: sys.path.insert(0, support) import foo.bar.buz - + conf = Config() opt = Bucket() plug = Doctest() plug.can_configure = True plug.configure(opt, conf) - suite = plug.loadTestsFromModule(foo.bar.buz) + suite = plug.loadTestsFromModule(foo.bar.buz) expect = ['[afunc (foo.bar.buz)]'] for test in suite: self.assertEqual(str(test), expect.pop(0)) @@ -163,7 +163,7 @@ def test_addresses(self): if not support in sys.path: sys.path.insert(0, support) import foo.bar.buz - + conf = Config() opt = Bucket() plug = Doctest() @@ -180,13 +180,13 @@ def test_addresses(self): file, mod, call = case.address() self.assertEqual(mod, 'foo.bar.buz') self.assertEqual(call, 'afunc') - + def test_collect_txtfile(self): here = os.path.abspath(os.path.dirname(__file__)) support = os.path.join(here, 'support') fn = os.path.join(support, 'foo', 'doctests.txt') - - conf = Config() + + conf = Config() opt = Bucket() plug = Doctest() plug.can_configure = True @@ -196,9 +196,9 @@ def test_collect_txtfile(self): for test in suite: assert str(test).endswith('doctests.txt') assert test.address(), "Test %s has no address" - + def test_collect_no_collect(self): - # bug http://nose.python-hosting.com/ticket/55 + # bug http://nose.python-hosting.com/ticket/55 # we got "iteration over non-sequence" when no files match here = os.path.abspath(os.path.dirname(__file__)) support = os.path.join(here, 'support') @@ -218,7 +218,12 @@ def test_add_options(self): {'dest': 'attr', 'action': 'append', 'default': None, 'metavar': 'ATTR', 'help': 'Run only tests that have attributes ' - 'specified by ATTR [NOSE_ATTR]'})] + 'specified by ATTR [NOSE_ATTR]'}), + (('--verbose-skipped-attr',), + {'dest': 'verbose_skipped_attr', 'default': False, + 'action': 'store_true', + 'help': 'Show debug messages when the test case is skipped'}) + ] if compat_24: expect.append( @@ -228,6 +233,7 @@ def test_add_options(self): 'help': 'Run only tests for whose attributes the ' 'Python expression EXPR evaluates to True ' '[NOSE_EVAL_ATTR]'})) + print(parser.opts) self.assertEqual(parser.opts, expect) opt = Bucket() @@ -246,14 +252,14 @@ def test_add_options(self): opt.attr = [ 'something,' ] plug.configure(opt, Config()) self.assertEqual(plug.attribs, [[('something', True)]] ) - + if compat_24: opt.attr = None opt.eval_attr = [ 'weird >= 66' ] plug.configure(opt, Config()) self.assertEqual(plug.attribs[0][0][0], 'weird >= 66') assert callable(plug.attribs[0][0][1]) - + def test_basic_attr(self): def f(): pass @@ -261,7 +267,7 @@ def f(): def g(): pass - + plug = AttributeSelector() plug.attribs = [[('a', True)]] assert plug.wantFunction(f) is not False @@ -275,12 +281,12 @@ def h(): def i(): pass - + plug = AttributeSelector() plug.attribs = [[('foo', True)]] assert plug.wantMethod(unbound_method(TestP, TestP.h)) is not False assert plug.wantFunction(i) is False - + def test_eval_attr(self): if not compat_24: warn("No support for eval attributes in python versions older" @@ -289,7 +295,7 @@ def test_eval_attr(self): def f(): pass f.monkey = 2 - + def g(): pass g.monkey = 6 @@ -297,7 +303,7 @@ def g(): def h(): pass h.monkey = 5 - + cnf = Config() opt = Bucket() opt.eval_attr = "monkey > 5" @@ -324,7 +330,7 @@ def f3(): def f4(): pass f4.tags = ['c', 'd'] - + cnf = Config() parser = OptionParser() plug = AttributeSelector() @@ -351,7 +357,7 @@ def f4(): assert not plug.wantFunction(f2) assert not plug.wantFunction(f3) assert not plug.wantFunction(f4) - + class TestFailureDetailPlugin(unittest.TestCase): @@ -377,7 +383,7 @@ class DummyError(Exception): class TestProfPlugin(unittest.TestCase): - def setUp(self): + def setUp(self): if not Profile.available(): raise SkipTest('profile plugin not available; skipping') @@ -409,7 +415,7 @@ def runcall(self, f, r): r[1] = f(), "wrapped" def func(): return "func" - + plug = Profile() plug.prof = dummy() result = plug.prepareTest(func)