3 new commits in pytest: https://bitbucket.org/hpk42/pytest/commits/a016be612113/ Changeset: a016be612113 User: hpk42 Date: 2014-03-11 22:10:17 Summary: introduce warning system with this API:
- node.warn() for a node-specific warning - config.warn() for a global non-node specific warning Each warning is accompanied by a "warning number" so that we can later introduce mechanisms for surpressing them. Each warning will trigger a call to pytest_report_warn(number, node, message) which is by default implemented by the TerminalReporter which introduces a new option "-rw" to show details about warnings. Affected #: 5 files diff -r 8d2d44cf0345a9e4e5992768c50c5918b8dc08cc -r a016be6121137d7e9bf6c534a7070e5cf1ee267b _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -613,6 +613,11 @@ self.hook.pytest_unconfigure(config=self) self.pluginmanager.ensure_shutdown() + def warn(self, code, message): + """ generate a warning for this test session. """ + self.hook.pytest_logwarning(code=code, message=message, + fslocation=None, nodeid=None) + def pytest_cmdline_parse(self, pluginmanager, args): assert self == pluginmanager.config, (self, pluginmanager.config) self.parse(args) diff -r 8d2d44cf0345a9e4e5992768c50c5918b8dc08cc -r a016be6121137d7e9bf6c534a7070e5cf1ee267b _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -227,6 +227,11 @@ def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ +def pytest_logwarning(message, code, nodeid, fslocation): + """ process a warning specified by a message, a code string, + a nodeid and fslocation (both of which may be None + if the warning is not tied to a partilar node/location).""" + # ------------------------------------------------------------------------- # doctest hooks # ------------------------------------------------------------------------- diff -r 8d2d44cf0345a9e4e5992768c50c5918b8dc08cc -r a016be6121137d7e9bf6c534a7070e5cf1ee267b _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -263,6 +263,20 @@ return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) + def warn(self, code, message): + """ generate a warning with the given code and message for this + item. """ + assert isinstance(code, str) + fslocation = getattr(self, "location", None) + if fslocation is None: + fslocation = getattr(self, "fspath", None) + else: + fslocation = "%s:%s" % fslocation[:2] + + self.ihook.pytest_logwarning(code=code, message=message, + nodeid=self.nodeid, + fslocation=fslocation) + # methods for ordering nodes @property def nodeid(self): diff -r 8d2d44cf0345a9e4e5992768c50c5918b8dc08cc -r a016be6121137d7e9bf6c534a7070e5cf1ee267b _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -75,6 +75,14 @@ letter = "f" return report.outcome, letter, report.outcome.upper() +class WarningReport: + def __init__(self, code, message, nodeid=None, fslocation=None): + self.code = code + self.message = message + self.nodeid = nodeid + self.fslocation = fslocation + + class TerminalReporter: def __init__(self, config, file=None): self.config = config @@ -151,6 +159,12 @@ self.write_line("INTERNALERROR> " + line) return 1 + def pytest_logwarning(self, code, fslocation, message, nodeid): + warnings = self.stats.setdefault("warnings", []) + warning = WarningReport(code=code, fslocation=fslocation, + message=message, nodeid=nodeid) + warnings.append(warning) + def pytest_plugin_registered(self, plugin): if self.config.option.traceconfig: msg = "PLUGIN registered: %s" % (plugin,) @@ -335,6 +349,7 @@ self.summary_errors() self.summary_failures() self.summary_hints() + self.summary_warnings() self.config.hook.pytest_terminal_summary(terminalreporter=self) if exitstatus == 2: self._report_keyboardinterrupt() @@ -405,6 +420,16 @@ for hint in self.config.pluginmanager._hints: self._tw.line("hint: %s" % hint) + def summary_warnings(self): + if self.hasopt("w"): + warnings = self.stats.get("warnings") + if not warnings: + return + self.write_sep("=", "warning summary") + for w in warnings: + self._tw.line("W%s %s %s" % (w.code, + w.fslocation, w.message)) + def summary_failures(self): if self.config.option.tbstyle != "no": reports = self.getreports('failed') @@ -449,7 +474,8 @@ def summary_stats(self): session_duration = py.std.time.time() - self._sessionstarttime - keys = "failed passed skipped deselected xfailed xpassed".split() + keys = ("failed passed skipped deselected " + "xfailed xpassed warnings").split() for key in self.stats.keys(): if key not in keys: keys.append(key) diff -r 8d2d44cf0345a9e4e5992768c50c5918b8dc08cc -r a016be6121137d7e9bf6c534a7070e5cf1ee267b testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -360,4 +360,43 @@ assert l[-2] == m.pytest_load_initial_conftests assert l[-3].__module__ == "_pytest.config" +class TestWarning: + def test_warn_config(self, testdir): + testdir.makeconftest(""" + l = [] + def pytest_configure(config): + config.warn("C1", "hello") + def pytest_logwarning(code, message): + assert code == "C1" + assert message == "hello" + l.append(1) + """) + testdir.makepyfile(""" + def test_proper(pytestconfig): + import conftest + assert conftest.l == [1] + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_warn_on_test_item_from_request(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.fixture + def fix(request): + request.node.warn("T1", "hello") + def test_hello(fix): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + *1 warning* + """) + assert "hello" not in result.stdout.str() + result = testdir.runpytest("-rw") + result.stdout.fnmatch_lines(""" + ===*warning summary*=== + *WT1*test_warn_on_test_item*:5*hello* + *1 warning* + """) https://bitbucket.org/hpk42/pytest/commits/a1ff8ba50ac5/ Changeset: a1ff8ba50ac5 User: hpk42 Date: 2014-03-11 22:10:18 Summary: warn if instances are callable and have a test name Affected #: 3 files diff -r a016be6121137d7e9bf6c534a7070e5cf1ee267b -r a1ff8ba50ac5617a2151a162f31887de7e48f761 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -11,7 +11,8 @@ NoneType = type(None) NOTSET = object() - +isfunction = inspect.isfunction +isclass = inspect.isclass callable = py.builtin.callable def getfslineno(obj): @@ -44,7 +45,7 @@ self.ids = ids def __call__(self, function): - if inspect.isclass(function): + if isclass(function): raise ValueError( "class fixtures not supported (may be in the future)") function._pytestfixturefunction = self @@ -213,14 +214,19 @@ res = __multicall__.execute() if res is not None: return res - if inspect.isclass(obj): + if isclass(obj): #if hasattr(collector.obj, 'unittest'): # return # we assume it's a mixin class for a TestCase derived one if collector.classnamefilter(name): Class = collector._getcustomclass("Class") return Class(name, parent=collector) - elif collector.funcnamefilter(name) and hasattr(obj, '__call__') and \ + elif collector.funcnamefilter(name) and hasattr(obj, "__call__") and \ getfixturemarker(obj) is None: + if not isfunction(obj): + collector.warn(code="C2", message= + "cannot collect %r because it is not a function." + % name, ) + return if is_generator(obj): return Generator(name, parent=collector) else: @@ -498,10 +504,9 @@ """ Collector for test methods. """ def collect(self): if hasinit(self.obj): - pytest.skip("class %s.%s with __init__ won't get collected" % ( - self.obj.__module__, - self.obj.__name__, - )) + self.warn("C1", "cannot collect test class %r because it has a " + "__init__ constructor" % self.obj.__name__) + return [] return [self._getcustomclass("Instance")(name="()", parent=self)] def setup(self): diff -r a016be6121137d7e9bf6c534a7070e5cf1ee267b -r a1ff8ba50ac5617a2151a162f31887de7e48f761 _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -15,7 +15,7 @@ group._addoption('-r', action="store", dest="reportchars", default=None, metavar="chars", help="show extra test summary info as specified by chars (f)ailed, " - "(E)error, (s)skipped, (x)failed, (X)passed.") + "(E)error, (s)skipped, (x)failed, (X)passed (w)warnings.") group._addoption('-l', '--showlocals', action="store_true", dest="showlocals", default=False, help="show locals in tracebacks (disabled by default).") diff -r a016be6121137d7e9bf6c534a7070e5cf1ee267b -r a1ff8ba50ac5617a2151a162f31887de7e48f761 testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -33,8 +33,8 @@ pytest.raises(ImportError, lambda: modcol.obj) class TestClass: - def test_class_with_init_skip_collect(self, testdir): - modcol = testdir.getmodulecol(""" + def test_class_with_init_warning(self, testdir): + testdir.makepyfile(""" class TestClass1: def __init__(self): pass @@ -42,11 +42,11 @@ def __init__(self): pass """) - l = modcol.collect() - assert len(l) == 2 - - for classcol in l: - pytest.raises(pytest.skip.Exception, classcol.collect) + result = testdir.runpytest("-rw") + result.stdout.fnmatch_lines(""" + WC1*test_class_with_init_warning.py*__init__* + *2 warnings* + """) def test_class_subclassobject(self, testdir): testdir.getmodulecol(""" @@ -276,6 +276,17 @@ assert isinstance(modcol, pytest.Module) assert hasattr(modcol.obj, 'test_func') + def test_function_as_object_instance_ignored(self, testdir): + item = testdir.makepyfile(""" + class A: + def __call__(self, tmpdir): + 0/0 + + test_a = A() + """) + reprec = testdir.inline_run() + reprec.assertoutcome() + def test_function_equality(self, testdir, tmpdir): from _pytest.python import FixtureManager config = testdir.parseconfigure() https://bitbucket.org/hpk42/pytest/commits/e18da3213547/ Changeset: e18da3213547 User: hpk42 Date: 2014-03-11 22:10:51 Summary: shrink and merge the somewhat obscure and undocumented internal hinting system with the new warnings one Affected #: 6 files diff -r a1ff8ba50ac5617a2151a162f31887de7e48f761 -r e18da321354757558ffd286a07e94d2b1cd42b18 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -82,6 +82,9 @@ config.addinivalue_line("markers", "trylast: mark a hook implementation function such that the " "plugin machinery will try to call it last/as late as possible.") + while self._warnings: + warning = self._warnings.pop(0) + config.warn(code="I1", message=warning) class Parser: @@ -94,7 +97,6 @@ self._usage = usage self._inidict = {} self._ininames = [] - self.hints = [] def processoption(self, option): if self._processopt: @@ -379,14 +381,6 @@ py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage, add_help=False, formatter_class=DropShorterLongHelpFormatter) - def format_epilog(self, formatter): - hints = self._parser.hints - if hints: - s = "\n".join(["hint: " + x for x in hints]) + "\n" - s = "\n" + s + "\n" - return s - return "" - def parse_args(self, args=None, namespace=None): """allow splitting of positional arguments""" args, argv = self.parse_known_args(args, namespace) @@ -716,7 +710,6 @@ self._preparse(args) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) - self._parser.hints.extend(self.pluginmanager._hints) args = self._parser.parse_setoption(args, self.option) if not args: args.append(py.std.os.getcwd()) diff -r a1ff8ba50ac5617a2151a162f31887de7e48f761 -r e18da321354757558ffd286a07e94d2b1cd42b18 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -71,7 +71,7 @@ self._name2plugin = {} self._listattrcache = {} self._plugins = [] - self._hints = [] + self._warnings = [] self.trace = TagTracer().get("pluginmanage") self._plugin_distinfo = [] self._shutdown = [] @@ -225,7 +225,7 @@ raise elif not isinstance(e, py.test.skip.Exception): raise - self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) + self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) else: self.register(mod, modname) self.consider_module(mod) diff -r a1ff8ba50ac5617a2151a162f31887de7e48f761 -r e18da321354757558ffd286a07e94d2b1cd42b18 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -64,7 +64,6 @@ def showhelp(config): tw = py.io.TerminalWriter() tw.write(config._parser.optparser.format_help()) - tw.write(config._parser.optparser.format_epilog(None)) tw.line() tw.line() #tw.sep( "=", "config file settings") @@ -86,6 +85,8 @@ tw.line("to see available fixtures type: py.test --fixtures") tw.line("(shown according to specified file_or_dir or current dir " "if not specified)") + for warning in config.pluginmanager._warnings: + tw.line("warning: %s" % (warning,)) return tw.line("conftest.py options:") diff -r a1ff8ba50ac5617a2151a162f31887de7e48f761 -r e18da321354757558ffd286a07e94d2b1cd42b18 _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -348,7 +348,6 @@ if exitstatus in (0, 1, 2, 4): self.summary_errors() self.summary_failures() - self.summary_hints() self.summary_warnings() self.config.hook.pytest_terminal_summary(terminalreporter=self) if exitstatus == 2: @@ -415,11 +414,6 @@ l.append(x) return l - def summary_hints(self): - if self.config.option.traceconfig: - for hint in self.config.pluginmanager._hints: - self._tw.line("hint: %s" % hint) - def summary_warnings(self): if self.hasopt("w"): warnings = self.stats.get("warnings") diff -r a1ff8ba50ac5617a2151a162f31887de7e48f761 -r e18da321354757558ffd286a07e94d2b1cd42b18 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -43,11 +43,11 @@ """) p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-p", "skipping1", "--traceconfig") + result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") assert result.ret == 0 result.stdout.fnmatch_lines([ - "*hint*skipping1*hello*", - "*hint*skipping2*hello*", + "WI1*skipped plugin*skipping1*hello*", + "WI1*skipped plugin*skipping2*hello*", ]) def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): diff -r a1ff8ba50ac5617a2151a162f31887de7e48f761 -r e18da321354757558ffd286a07e94d2b1cd42b18 testing/test_parseopt.py --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -236,16 +236,6 @@ help = parser.optparser.format_help() assert '-doit, --func-args foo' in help -@pytest.mark.skipif("sys.version_info < (2,5)") -def test_addoption_parser_epilog(testdir): - testdir.makeconftest(""" - def pytest_addoption(parser): - parser.hints.append("hello world") - parser.hints.append("from me too") - """) - result = testdir.runpytest('--help') - #assert result.ret != 0 - result.stdout.fnmatch_lines(["hint: hello world", "hint: from me too"]) @pytest.mark.skipif("sys.version_info < (2,6)") def test_argcomplete(testdir, monkeypatch): Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ pytest-commit mailing list pytest-commit@python.org https://mail.python.org/mailman/listinfo/pytest-commit