From commits-noreply at bitbucket.org Mon Dec 6 16:57:23 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 6 Dec 2010 09:57:23 -0600 (CST) Subject: [py-svn] pytest commit 1c3eb86502b3: fix issue8 : avoid errors caused by logging module wanting to close already closed streams. Message-ID: <20101206155723.147741E13BF@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291650972 -3600 # Node ID 1c3eb86502b30a52e8adf798967b7d547edd8f43 # Parent fb3765d0cc9e9e44f1b2d7a5c99d3c389d9ee214 fix issue8 : avoid errors caused by logging module wanting to close already closed streams. The issue arose if logging was initialized while capturing was enabled and then capturing streams were closed before process exit, leading to the logging module to complain. --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -306,6 +306,26 @@ class TestLoggingInteraction: # verify proper termination assert "closed" not in s + def test_logging_initialized_in_test(self, testdir): + p = testdir.makepyfile(""" + import sys + def test_something(): + # pytest does not import logging + assert 'logging' not in sys.modules + import logging + logging.basicConfig() + logging.warn("hello432") + assert 0 + """) + result = testdir.runpytest(p, "--traceconfig", + "-p", "no:capturelog") + assert result.ret != 0 + result.stdout.fnmatch_lines([ + "*hello432*", + ]) + assert 'operation on closed file' not in result.stderr.str() + + class TestCaptureFuncarg: def test_std_functional(self, testdir): reprec = testdir.inline_runsource(""" --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -26,7 +26,9 @@ def pytest_unconfigure(config): capman = config.pluginmanager.getplugin('capturemanager') while capman._method2capture: name, cap = capman._method2capture.popitem() - cap.reset() + # XXX logging module may wants to close it itself on process exit + # otherwise we could do finalization here and call "reset()". + cap.suspend() class NoCapture: def startall(self): --- a/_pytest/core.py +++ b/_pytest/core.py @@ -151,15 +151,15 @@ class PluginManager(object): except ImportError: return # XXX issue a warning for ep in iter_entry_points('pytest11'): - if ep.name in self._name2plugin: + name = ep.name + if name.startswith("pytest_"): + name = name[7:] + if ep.name in self._name2plugin or name in self._name2plugin: continue try: plugin = ep.load() except DistributionNotFound: continue - name = ep.name - if name.startswith("pytest_"): - name = name[7:] self.register(plugin, name=name) def consider_preparse(self, args): --- a/tox.ini +++ b/tox.ini @@ -73,3 +73,4 @@ rsyncdirs=tox.ini pytest.py _pytest test python_files=test_*.py *_test.py python_classes=Test Acceptance python_functions=test +pep8ignore = E401 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- +- fix issue8: no logging errors at process exit - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the From commits-noreply at bitbucket.org Mon Dec 6 16:57:22 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 6 Dec 2010 09:57:22 -0600 (CST) Subject: [py-svn] pytest commit 127ccc0c6fcb: add some docs and new projects Message-ID: <20101206155722.CB2E51E135E@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291628480 -3600 # Node ID 127ccc0c6fcb51eb3d316be26df808cc14a73bc9 # Parent 0016d19f02853b7b1f18bb170162e4ffb994fe1b add some docs and new projects --- a/doc/capture.txt +++ b/doc/capture.txt @@ -12,6 +12,47 @@ attempts to read from it. This is impor test otherwise might lead to waiting for input - which is usually not desired when running automated tests. +.. _printdebugging: + +Using print statements for debugging +--------------------------------------------------- + +One primary benefit of the default capturing of stdout/stderr output +is that you can use print statements for debugging:: + + # content of test_module.py + + def setup_function(function): + print ("setting up %s" % function) + + def test_func1(): + assert True + + def test_func2(): + assert False + +and running this module will show you precisely the output +of the failing function and hide the other one:: + + $ py.test + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.1.dev1 + collecting ... collected 2 items + + test_module.py .F + + ================================= FAILURES ================================= + ________________________________ test_func2 ________________________________ + + def test_func2(): + > assert False + E assert False + + test_module.py:9: AssertionError + ----------------------------- Captured stdout ------------------------------ + setting up + ==================== 1 failed, 1 passed in 0.04 seconds ==================== + Setting capturing methods or disabling capturing ------------------------------------------------- @@ -35,14 +76,6 @@ You can influence output capturing mecha py.test --capture=sys # replace sys.stdout/stderr with in-mem files py.test --capture=fd # also point filedescriptors 1 and 2 to temp file -If you set capturing values in a conftest file like this:: - - # conftest.py - option_capture = 'fd' - -then all tests in that directory will execute with "fd" style capturing. - -_ `printdebugging`: Accessing captured output from a test function --------------------------------------------------- --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ basepython=python2.7 deps=pytest-xdist commands= py.test -n3 -rfsxX \ - --junitxml={envlogdir}/junit-{envname}.xml [] + --ignore .tox --junitxml={envlogdir}/junit-{envname}.xml [] [testenv:trial] changedir=. @@ -68,7 +68,7 @@ commands= [pytest] minversion=2.0 plugins=pytester -addopts= -rxf --pyargs --doctest-modules +addopts= -rxf --pyargs --doctest-modules --ignore=.tox rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py python_classes=Test Acceptance --- a/doc/projects.txt +++ b/doc/projects.txt @@ -3,18 +3,21 @@ Project examples ========================== -Here are some examples of projects using py.test: +Here are some examples of projects using py.test (please send notes via :ref:`contact`): * `PyPy `_, Python with a JIT compiler, running over `16000 tests `_ * the `MoinMoin `_ Wiki Engine * `tox `_, virtualenv/Hudson integration tool * `PIDA `_ framework for integrated development +* `PyPM `_ ActiveState's package manager * `Fom `_ a fluid object mapper for FluidDB +* `applib `_ cross-platform utilities * `six `_ Python 2 and 3 compatibility utilities * `pediapress `_ MediaWiki articles * `mwlib `_ mediawiki parser and utility library * `The Translate Toolkit `_ for localization and conversion * `execnet `_ rapid multi-Python deployment +* `pylib `_ cross-platform path, IO, dynamic code library * `Pacha `_ configuration management in five minutes * `bbfreeze `_ create standalone executables from Python scripts * `pdb++ `_ a fancier version of PDB @@ -34,15 +37,15 @@ Here are some examples of projects using * `bu `_ a microscopic build system * `katcp `_ Telescope communication protocol over Twisted * `kss plugin timer `_ -* many more ... (please send notes via the :ref:`contact`) Some organisations using py.test ----------------------------------- -* `Square Kilometre Array `_ +* `Square Kilometre Array, Cape Town `_ * `Tandberg `_ -* `Stups department of Heinrich Heine University `_ -* `Open End `_ -* `Laboraratory of Bioinformatics `_ -* `merlinux `_ +* `Shootq `_ +* `Stups department of Heinrich Heine University D??sseldorf `_ +* `Open End, Gotenborg `_ +* `Laboraratory of Bioinformatics, Warsaw `_ +* `merlinux, Germany `_ * many more ... (please be so kind to send a note via :ref:`contact`) --- a/doc/index.txt +++ b/doc/index.txt @@ -8,7 +8,7 @@ Welcome to ``py.test``! - runs on Posix/Windows, Python 2.4-3.2, PyPy and Jython - continously `tested on many Python interpreters `_ - - used in :ref:`many projects `, ranging from 10 to 10000 tests + - used in :ref:`many projects and organisations `, ranging from 10 to 10000 tests - has :ref:`comprehensive documentation ` - comes with :ref:`tested examples ` - supports :ref:`good integration practises ` @@ -18,7 +18,8 @@ Welcome to ``py.test``! - makes it :ref:`easy to get started `, refined :ref:`usage options ` - :ref:`assert with the assert statement` - helpful :ref:`traceback and failing assertion reporting ` - - allows `print debugging `_ and `generic output capturing `_ + - allows :ref:`print debugging ` and :ref:`generic output + capturing ` - supports :pep:`8` compliant coding style in tests - **supports functional testing and complex test setups** --- a/doc/xdist.txt +++ b/doc/xdist.txt @@ -148,7 +148,7 @@ environment this command will send each platforms - and report back failures from all platforms at once. The specifications strings use the `xspec syntax`_. -.. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec +.. _`xspec syntax`: http://codespeak.net/execnet/basics.html#xspec .. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py --- a/doc/contact.txt +++ b/doc/contact.txt @@ -15,7 +15,7 @@ Contact channels - #pylib on irc.freenode.net IRC channel for random questions. -- private mail to Holger.Krekel at gmail com if you have sensitive issues to communicate +- private mail to Holger.Krekel at gmail com if you want to communicate sensitive issues - `commit mailing list`_ From commits-noreply at bitbucket.org Mon Dec 6 16:57:23 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 6 Dec 2010 09:57:23 -0600 (CST) Subject: [py-svn] pytest commit fb3765d0cc9e: refine plugin registration, allow new "-p no:NAME" way to prevent/undo plugin registration Message-ID: <20101206155723.0441F1E13BD@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291650882 -3600 # Node ID fb3765d0cc9e9e44f1b2d7a5c99d3c389d9ee214 # Parent 127ccc0c6fcb51eb3d316be26df808cc14a73bc9 refine plugin registration, allow new "-p no:NAME" way to prevent/undo plugin registration --- a/doc/links.inc +++ b/doc/links.inc @@ -10,7 +10,6 @@ .. _pytest: http://pypi.python.org/pypi/pytest .. _mercurial: http://mercurial.selenic.com/wiki/ .. _`setuptools`: http://pypi.python.org/pypi/setuptools - .. _`easy_install`: .. _`distribute docs`: .. _`distribute`: http://pypi.python.org/pypi/distribute --- a/testing/test_session.py +++ b/testing/test_session.py @@ -212,8 +212,8 @@ def test_plugin_specify(testdir): #) def test_plugin_already_exists(testdir): - config = testdir.parseconfig("-p", "session") - assert config.option.plugins == ['session'] + config = testdir.parseconfig("-p", "terminal") + assert config.option.plugins == ['terminal'] config.pluginmanager.do_configure(config) def test_exclude(testdir): --- /dev/null +++ b/_pytest/main.py @@ -0,0 +1,516 @@ +""" core implementation of testing process: init, session, runtest loop. """ + +import py +import pytest, _pytest +import os, sys +tracebackcutdir = py.path.local(_pytest.__file__).dirpath() + +# exitcodes for the command line +EXIT_OK = 0 +EXIT_TESTSFAILED = 1 +EXIT_INTERRUPTED = 2 +EXIT_INTERNALERROR = 3 + +def pytest_addoption(parser): + parser.addini("norecursedirs", "directory patterns to avoid for recursion", + type="args", default=('.*', 'CVS', '_darcs', '{arch}')) + #parser.addini("dirpatterns", + # "patterns specifying possible locations of test files", + # type="linelist", default=["**/test_*.txt", + # "**/test_*.py", "**/*_test.py"] + #) + group = parser.getgroup("general", "running and selection options") + group._addoption('-x', '--exitfirst', action="store_true", default=False, + dest="exitfirst", + help="exit instantly on first error or failed test."), + group._addoption('--maxfail', metavar="num", + action="store", type="int", dest="maxfail", default=0, + help="exit after first num failures or errors.") + + group = parser.getgroup("collect", "collection") + group.addoption('--collectonly', + action="store_true", dest="collectonly", + help="only collect tests, don't execute them."), + group.addoption('--pyargs', action="store_true", + help="try to interpret all arguments as python packages.") + group.addoption("--ignore", action="append", metavar="path", + help="ignore path during collection (multi-allowed).") + group.addoption('--confcutdir', dest="confcutdir", default=None, + metavar="dir", + help="only load conftest.py's relative to specified dir.") + + group = parser.getgroup("debugconfig", + "test session debugging and configuration") + group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", + help="base temporary directory for this test run.") + + +def pytest_namespace(): + return dict(collect=dict(Item=Item, Collector=Collector, File=File)) + +def pytest_configure(config): + py.test.config = config # compatibiltiy + if config.option.exitfirst: + config.option.maxfail = 1 + +def pytest_cmdline_main(config): + """ default command line protocol for initialization, session, + running tests and reporting. """ + session = Session(config) + session.exitstatus = EXIT_OK + try: + config.pluginmanager.do_configure(config) + config.hook.pytest_sessionstart(session=session) + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + except pytest.UsageError: + raise + except KeyboardInterrupt: + excinfo = py.code.ExceptionInfo() + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = EXIT_INTERRUPTED + except: + excinfo = py.code.ExceptionInfo() + config.pluginmanager.notify_exception(excinfo) + session.exitstatus = EXIT_INTERNALERROR + if excinfo.errisinstance(SystemExit): + sys.stderr.write("mainloop: caught Spurious SystemExit!\n") + if not session.exitstatus and session._testsfailed: + session.exitstatus = EXIT_TESTSFAILED + config.hook.pytest_sessionfinish(session=session, + exitstatus=session.exitstatus) + config.pluginmanager.do_unconfigure(config) + return session.exitstatus + +def pytest_collection(session): + session.perform_collect() + hook = session.config.hook + hook.pytest_collection_modifyitems(session=session, + config=session.config, items=session.items) + hook.pytest_collection_finish(session=session) + return True + +def pytest_runtestloop(session): + if session.config.option.collectonly: + return True + for item in session.session.items: + item.config.hook.pytest_runtest_protocol(item=item) + if session.shouldstop: + raise session.Interrupted(session.shouldstop) + return True + +def pytest_ignore_collect(path, config): + p = path.dirpath() + ignore_paths = config._getconftest_pathlist("collect_ignore", path=p) + ignore_paths = ignore_paths or [] + excludeopt = config.getvalue("ignore") + if excludeopt: + ignore_paths.extend([py.path.local(x) for x in excludeopt]) + return path in ignore_paths + +class HookProxy: + def __init__(self, fspath, config): + self.fspath = fspath + self.config = config + def __getattr__(self, name): + hookmethod = getattr(self.config.hook, name) + def call_matching_hooks(**kwargs): + plugins = self.config._getmatchingplugins(self.fspath) + return hookmethod.pcall(plugins, **kwargs) + return call_matching_hooks + +def compatproperty(name): + def fget(self): + #print "retrieving %r property from %s" %(name, self.fspath) + py.log._apiwarn("2.0", "use pytest.%s for " + "test collection and item classes" % name) + return getattr(pytest, name) + return property(fget, None, None, + "deprecated attribute %r, use pytest.%s" % (name,name)) + +class Node(object): + """ base class for all Nodes in the collection tree. + Collector subclasses have children, Items are terminal nodes.""" + + def __init__(self, name, parent=None, config=None, session=None): + #: a unique name with the scope of the parent + self.name = name + + #: the parent collector node. + self.parent = parent + + #: the test config object + self.config = config or parent.config + + #: the collection this node is part of + self.session = session or parent.session + + #: filesystem path where this node was collected from + self.fspath = getattr(parent, 'fspath', None) + self.ihook = self.session.gethookproxy(self.fspath) + self.keywords = {self.name: True} + + Module = compatproperty("Module") + Class = compatproperty("Class") + Function = compatproperty("Function") + File = compatproperty("File") + Item = compatproperty("Item") + + def __repr__(self): + return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) + + # methods for ordering nodes + @property + def nodeid(self): + try: + return self._nodeid + except AttributeError: + self._nodeid = x = self._makeid() + return x + + def _makeid(self): + return self.parent.nodeid + "::" + self.name + + def __eq__(self, other): + if not isinstance(other, Node): + return False + return self.__class__ == other.__class__ and \ + self.name == other.name and self.parent == other.parent + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.name, self.parent)) + + def setup(self): + pass + + def teardown(self): + pass + + def _memoizedcall(self, attrname, function): + exattrname = "_ex_" + attrname + failure = getattr(self, exattrname, None) + if failure is not None: + py.builtin._reraise(failure[0], failure[1], failure[2]) + if hasattr(self, attrname): + return getattr(self, attrname) + try: + res = function() + except py.builtin._sysex: + raise + except: + failure = py.std.sys.exc_info() + setattr(self, exattrname, failure) + raise + setattr(self, attrname, res) + return res + + def listchain(self): + """ return list of all parent collectors up to self, + starting from root of collection tree. """ + l = [self] + while 1: + x = l[0] + if x.parent is not None: # and x.parent.parent is not None: + l.insert(0, x.parent) + else: + return l + + def listnames(self): + return [x.name for x in self.listchain()] + + def getplugins(self): + return self.config._getmatchingplugins(self.fspath) + + def getparent(self, cls): + current = self + while current and not isinstance(current, cls): + current = current.parent + return current + + def _prunetraceback(self, excinfo): + pass + + def _repr_failure_py(self, excinfo, style=None): + if self.config.option.fulltrace: + style="long" + else: + self._prunetraceback(excinfo) + # XXX should excinfo.getrepr record all data and toterminal() + # process it? + if style is None: + if self.config.option.tbstyle == "short": + style = "short" + else: + style = "long" + return excinfo.getrepr(funcargs=True, + showlocals=self.config.option.showlocals, + style=style) + + repr_failure = _repr_failure_py + +class Collector(Node): + """ Collector instances create children through collect() + and thus iteratively build a tree. + """ + class CollectError(Exception): + """ an error during collection, contains a custom message. """ + + def collect(self): + """ returns a list of children (items and collectors) + for this collection node. + """ + raise NotImplementedError("abstract") + + def repr_failure(self, excinfo): + """ represent a collection failure. """ + if excinfo.errisinstance(self.CollectError): + exc = excinfo.value + return str(exc.args[0]) + return self._repr_failure_py(excinfo, style="short") + + def _memocollect(self): + """ internal helper method to cache results of calling collect(). """ + return self._memoizedcall('_collected', lambda: list(self.collect())) + + def _prunetraceback(self, excinfo): + if hasattr(self, 'fspath'): + path = self.fspath + traceback = excinfo.traceback + ntraceback = traceback.cut(path=self.fspath) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=tracebackcutdir) + excinfo.traceback = ntraceback.filter() + +class FSCollector(Collector): + def __init__(self, fspath, parent=None, config=None, session=None): + fspath = py.path.local(fspath) # xxx only for test_resultlog.py? + name = fspath.basename + if parent is not None: + rel = fspath.relto(parent.fspath) + if rel: + name = rel + name = name.replace(os.sep, "/") + super(FSCollector, self).__init__(name, parent, config, session) + self.fspath = fspath + + def _makeid(self): + if self == self.session: + return "." + relpath = self.session.fspath.bestrelpath(self.fspath) + if os.sep != "/": + relpath = relpath.replace(os.sep, "/") + return relpath + +class File(FSCollector): + """ base class for collecting tests from a file. """ + +class Item(Node): + """ a basic test invocation item. Note that for a single function + there might be multiple test invocation items. + """ + def reportinfo(self): + return self.fspath, None, "" + + @property + def location(self): + try: + return self._location + except AttributeError: + location = self.reportinfo() + fspath = self.session.fspath.bestrelpath(location[0]) + location = (fspath, location[1], str(location[2])) + self._location = location + return location + +class NoMatch(Exception): + """ raised if matching cannot locate a matching names. """ + +class Session(FSCollector): + class Interrupted(KeyboardInterrupt): + """ signals an interrupted test run. """ + __module__ = 'builtins' # for py3 + + def __init__(self, config): + super(Session, self).__init__(py.path.local(), parent=None, + config=config, session=self) + assert self.config.pluginmanager.register(self, name="session", prepend=True) + self._testsfailed = 0 + self.shouldstop = False + self.trace = config.trace.root.get("collection") + self._norecursepatterns = config.getini("norecursedirs") + + def pytest_collectstart(self): + if self.shouldstop: + raise self.Interrupted(self.shouldstop) + + def pytest_runtest_logreport(self, report): + if report.failed and 'xfail' not in getattr(report, 'keywords', []): + self._testsfailed += 1 + maxfail = self.config.getvalue("maxfail") + if maxfail and self._testsfailed >= maxfail: + self.shouldstop = "stopping after %d failures" % ( + self._testsfailed) + pytest_collectreport = pytest_runtest_logreport + + def isinitpath(self, path): + return path in self._initialpaths + + def gethookproxy(self, fspath): + return HookProxy(fspath, self.config) + + def perform_collect(self, args=None, genitems=True): + if args is None: + args = self.config.args + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + self._notfound = [] + self._initialpaths = set() + self._initialparts = [] + for arg in args: + parts = self._parsearg(arg) + self._initialparts.append(parts) + self._initialpaths.add(parts[0]) + self.ihook.pytest_collectstart(collector=self) + rep = self.ihook.pytest_make_collect_report(collector=self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + for arg, exc in self._notfound: + line = "(no name %r in any of %r)" % (arg, exc.args[0]) + raise pytest.UsageError("not found: %s\n%s" %(arg, line)) + if not genitems: + return rep.result + else: + self.items = items = [] + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + return items + + def collect(self): + for parts in self._initialparts: + arg = "::".join(map(str, parts)) + self.trace("processing argument", arg) + self.trace.root.indent += 1 + try: + for x in self._collect(arg): + yield x + except NoMatch: + # we are inside a make_report hook so + # we cannot directly pass through the exception + self._notfound.append((arg, sys.exc_info()[1])) + self.trace.root.indent -= 1 + break + self.trace.root.indent -= 1 + + def _collect(self, arg): + names = self._parsearg(arg) + path = names.pop(0) + if path.check(dir=1): + assert not names, "invalid arg %r" %(arg,) + for path in path.visit(fil=lambda x: x.check(file=1), + rec=self._recurse, bf=True, sort=True): + for x in self._collectfile(path): + yield x + else: + assert path.check(file=1) + for x in self.matchnodes(self._collectfile(path), names): + yield x + + def _collectfile(self, path): + ihook = self.gethookproxy(path) + if not self.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + return ihook.pytest_collect_file(path=path, parent=self) + + def _recurse(self, path): + ihook = self.gethookproxy(path.dirpath()) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return + for pat in self._norecursepatterns: + if path.check(fnmatch=pat): + return False + ihook = self.gethookproxy(path) + ihook.pytest_collect_directory(path=path, parent=self) + return True + + def _tryconvertpyarg(self, x): + try: + mod = __import__(x, None, None, ['__doc__']) + except (ValueError, ImportError): + return x + p = py.path.local(mod.__file__) + if p.purebasename == "__init__": + p = p.dirpath() + else: + p = p.new(basename=p.purebasename+".py") + return p + + def _parsearg(self, arg): + """ return (fspath, names) tuple after checking the file exists. """ + arg = str(arg) + if self.config.option.pyargs: + arg = self._tryconvertpyarg(arg) + parts = str(arg).split("::") + relpath = parts[0].replace("/", os.sep) + path = self.fspath.join(relpath, abs=True) + if not path.check(): + if self.config.option.pyargs: + msg = "file or package not found: " + else: + msg = "file not found: " + raise pytest.UsageError(msg + arg) + parts[0] = path + return parts + + def matchnodes(self, matching, names): + self.trace("matchnodes", matching, names) + self.trace.root.indent += 1 + nodes = self._matchnodes(matching, names) + num = len(nodes) + self.trace("matchnodes finished -> ", num, "nodes") + self.trace.root.indent -= 1 + if num == 0: + raise NoMatch(matching, names[:1]) + return nodes + + def _matchnodes(self, matching, names): + if not matching or not names: + return matching + name = names[0] + assert name + nextnames = names[1:] + resultnodes = [] + for node in matching: + if isinstance(node, pytest.Item): + if not names: + resultnodes.append(node) + continue + assert isinstance(node, pytest.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for x in rep.result: + if x.name == name: + resultnodes.extend(self.matchnodes([x], nextnames)) + node.ihook.pytest_collectreport(report=rep) + return resultnodes + + def genitems(self, node): + self.trace("genitems", node) + if isinstance(node, pytest.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, pytest.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for subnode in rep.result: + for x in self.genitems(subnode): + yield x + node.ihook.pytest_collectreport(report=rep) --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,11 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- -- refinements to terminal output +- refinements to "collecting" output on non-ttys +- refine internal plugin registration and --traceconfig output +- introduce a mechanism to prevent/unregister plugins from the + command line, see http://pytest.org/plugins.html#cmdunregister +- activate resultlog plugin by default Changes between 1.3.4 and 2.0.0 ---------------------------------------------- --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -80,7 +80,11 @@ def pytest_report_header(config): plugins = [] items = config.pluginmanager._name2plugin.items() for name, plugin in items: - lines.append(" %-20s: %s" %(name, repr(plugin))) + if hasattr(plugin, '__file__'): + r = plugin.__file__ + else: + r = repr(plugin) + lines.append(" %-20s: %s" %(name, r)) return lines --- a/_pytest/python.py +++ b/_pytest/python.py @@ -730,7 +730,7 @@ class FuncargRequest: raise self.LookupError(msg) def showfuncargs(config): - from _pytest.session import Session + from _pytest.main import Session session = Session(config) session.perform_collect() if session.items: --- a/_pytest/core.py +++ b/_pytest/core.py @@ -11,11 +11,9 @@ assert py.__version__.split(".")[:2] >= "%s is too old, remove or upgrade 'py'" % (py.__version__)) default_plugins = ( - "config mark session terminal runner python pdb unittest capture skipping " + "config mark main terminal runner python pdb unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml doctest").split() - -IMPORTPREFIX = "pytest_" + "junitxml resultlog doctest").split() class TagTracer: def __init__(self, prefix="[pytest] "): @@ -79,20 +77,12 @@ class PluginManager(object): for spec in default_plugins: self.import_plugin(spec) - def _getpluginname(self, plugin, name): - if name is None: - if hasattr(plugin, '__name__'): - name = plugin.__name__.split(".")[-1] - else: - name = id(plugin) - return name - def register(self, plugin, name=None, prepend=False): assert not self.isregistered(plugin), plugin - assert not self.isregistered(plugin), plugin - name = self._getpluginname(plugin, name) + name = name or getattr(plugin, '__name__', str(id(plugin))) if name in self._name2plugin: return False + #self.trace("registering", name, plugin) self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) @@ -112,7 +102,7 @@ class PluginManager(object): del self._name2plugin[name] def isregistered(self, plugin, name=None): - if self._getpluginname(plugin, name) in self._name2plugin: + if self.getplugin(name) is not None: return True for val in self._name2plugin.values(): if plugin == val: @@ -136,11 +126,12 @@ class PluginManager(object): return False def getplugin(self, name): + if name is None: + return None try: return self._name2plugin[name] except KeyError: - impname = canonical_importname(name) - return self._name2plugin[impname] + return self._name2plugin.get("_pytest." + name, None) # API for bootstrapping # @@ -160,19 +151,28 @@ class PluginManager(object): except ImportError: return # XXX issue a warning for ep in iter_entry_points('pytest11'): - name = canonical_importname(ep.name) - if name in self._name2plugin: + if ep.name in self._name2plugin: continue try: plugin = ep.load() except DistributionNotFound: continue + name = ep.name + if name.startswith("pytest_"): + name = name[7:] self.register(plugin, name=name) def consider_preparse(self, args): for opt1,opt2 in zip(args, args[1:]): if opt1 == "-p": - self.import_plugin(opt2) + if opt2.startswith("no:"): + name = opt2[3:] + if self.getplugin(name) is not None: + self.unregister(None, name=name) + self._name2plugin[name] = -1 + else: + if self.getplugin(opt2) is None: + self.import_plugin(opt2) def consider_conftest(self, conftestmodule): if self.register(conftestmodule, name=conftestmodule.__file__): @@ -186,15 +186,19 @@ class PluginManager(object): for spec in attr: self.import_plugin(spec) - def import_plugin(self, spec): - assert isinstance(spec, str) - modname = canonical_importname(spec) - if modname in self._name2plugin: + def import_plugin(self, modname): + assert isinstance(modname, str) + if self.getplugin(modname) is not None: return try: + #self.trace("importing", modname) mod = importplugin(modname) except KeyboardInterrupt: raise + except ImportError: + if modname.startswith("pytest_"): + return self.import_plugin(modname[7:]) + raise except: e = py.std.sys.exc_info()[1] if not hasattr(py.test, 'skip'): @@ -290,34 +294,18 @@ class PluginManager(object): return MultiCall(methods=self.listattr(methname, plugins=[plugin]), kwargs=kwargs, firstresult=True).execute() -def canonical_importname(name): - if '.' in name: - return name - name = name.lower() - if not name.startswith(IMPORTPREFIX): - name = IMPORTPREFIX + name - return name def importplugin(importspec): - #print "importing", importspec + name = importspec try: - return __import__(importspec, None, None, '__doc__') + mod = "_pytest." + name + return __import__(mod, None, None, '__doc__') except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(importspec) == -1: - raise - name = importspec - try: - if name.startswith("pytest_"): - name = importspec[7:] - return __import__("_pytest.%s" %(name), None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(name) == -1: - raise - # show the original exception, not the failing internal one - return __import__(importspec, None, None, '__doc__') - + #e = py.std.sys.exc_info()[1] + #if str(e).find(name) == -1: + # raise + pass # + return __import__(importspec, None, None, '__doc__') class MultiCall: """ execute a call into multiple python functions/methods. """ --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -27,7 +27,7 @@ class TestGeneralUsage: def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """) - result = testdir.runpytest("-p", "xyz", "--xyz=123") + result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123") assert result.ret == 0 result.stdout.fnmatch_lines([ '*1 passed*', --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,5 +1,5 @@ import pytest, py, os -from _pytest.core import PluginManager, canonical_importname +from _pytest.core import PluginManager from _pytest.core import MultiCall, HookRelay, varnames @@ -15,30 +15,47 @@ class TestBootstrapping: pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) """) + def test_plugin_prevent_register(self): + pluginmanager = PluginManager() + pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) + l1 = pluginmanager.getplugins() + pluginmanager.register(42, name="abc") + l2 = pluginmanager.getplugins() + assert len(l2) == len(l1) + + def test_plugin_prevent_register_unregistered_alredy_registered(self): + pluginmanager = PluginManager() + pluginmanager.register(42, name="abc") + l1 = pluginmanager.getplugins() + assert 42 in l1 + pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) + l2 = pluginmanager.getplugins() + assert 42 not in l2 + def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(pytest_skipping1=""" + p = testdir.makepyfile(skipping1=""" import pytest pytest.skip("hello") """) - p.copy(p.dirpath("pytest_skipping2.py")) + p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") result = testdir.runpytest("-p", "skipping1", "--traceconfig") assert result.ret == 0 result.stdout.fnmatch_lines([ + "*hint*skipping1*hello*", "*hint*skipping2*hello*", - "*hint*skipping1*hello*", ]) def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): pluginmanager = PluginManager() testdir.syspathinsert() - testdir.makepyfile(pytest_xy123="#") + testdir.makepyfile(xy123="#") monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') l1 = len(pluginmanager.getplugins()) pluginmanager.consider_env() l2 = len(pluginmanager.getplugins()) assert l2 == l1 + 1 - assert pluginmanager.getplugin('pytest_xy123') + assert pluginmanager.getplugin('xy123') pluginmanager.consider_env() l3 = len(pluginmanager.getplugins()) assert l2 == l3 @@ -48,7 +65,7 @@ class TestBootstrapping: def my_iter(name): assert name == "pytest11" class EntryPoint: - name = "mytestplugin" + name = "pytest_mytestplugin" def load(self): class PseudoPlugin: x = 42 @@ -60,8 +77,6 @@ class TestBootstrapping: pluginmanager.consider_setuptools_entrypoints() plugin = pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 - plugin2 = pluginmanager.getplugin("pytest_mytestplugin") - assert plugin2 == plugin def test_consider_setuptools_not_installed(self, monkeypatch): monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', @@ -75,7 +90,7 @@ class TestBootstrapping: p = testdir.makepyfile(""" import pytest def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('x500') + plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') assert plugin is not None """) monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") @@ -91,14 +106,14 @@ class TestBootstrapping: reset = testdir.syspathinsert() pluginname = "pytest_hello" testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("hello") + pluginmanager.import_plugin("pytest_hello") len1 = len(pluginmanager.getplugins()) pluginmanager.import_plugin("pytest_hello") len2 = len(pluginmanager.getplugins()) assert len1 == len2 plugin1 = pluginmanager.getplugin("pytest_hello") assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pluginmanager.getplugin("hello") + plugin2 = pluginmanager.getplugin("pytest_hello") assert plugin2 is plugin1 def test_import_plugin_dotted_name(self, testdir): @@ -116,13 +131,13 @@ class TestBootstrapping: def test_consider_module(self, testdir): pluginmanager = PluginManager() testdir.syspathinsert() - testdir.makepyfile(pytest_plug1="#") - testdir.makepyfile(pytest_plug2="#") + testdir.makepyfile(pytest_p1="#") + testdir.makepyfile(pytest_p2="#") mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] + mod.pytest_plugins = ["pytest_p1", "pytest_p2"] pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" - assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" + assert pluginmanager.getplugin("pytest_p1").__name__ == "pytest_p1" + assert pluginmanager.getplugin("pytest_p2").__name__ == "pytest_p2" def test_consider_module_import_module(self, testdir): mod = py.std.types.ModuleType("x") @@ -198,8 +213,7 @@ class TestBootstrapping: mod = py.std.types.ModuleType("pytest_xyz") monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) pp = PluginManager() - pp.import_plugin('xyz') - assert pp.getplugin('xyz') == mod + pp.import_plugin('pytest_xyz') assert pp.getplugin('pytest_xyz') == mod assert pp.isregistered(mod) @@ -217,9 +231,6 @@ class TestBootstrapping: pass excinfo = pytest.raises(Exception, "pp.register(hello())") - def test_canonical_importname(self): - for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': - impname = canonical_importname(name) def test_notify_exception(self, capfd): pp = PluginManager() @@ -400,7 +411,7 @@ class TestPytestPluginInteractions: assert len(l) == 0 config.pluginmanager.do_configure(config=config) assert len(l) == 1 - config.pluginmanager.register(A()) # this should lead to a configured() plugin + config.pluginmanager.register(A()) # leads to a configured() plugin assert len(l) == 2 assert l[0] != l[1] --- a/_pytest/resultlog.py +++ b/_pytest/resultlog.py @@ -1,12 +1,12 @@ """ (disabled by default) create result information in a plain text file. """ import py -from py.builtin import print_ def pytest_addoption(parser): - group = parser.getgroup("resultlog", "resultlog plugin options") - group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", default=None, - help="path for machine-readable result log.") + group = parser.getgroup("terminal reporting", "resultlog plugin options") + group.addoption('--resultlog', action="store", dest="resultlog", + metavar="path", default=None, + help="path for machine-readable result log.") def pytest_configure(config): resultlog = config.option.resultlog @@ -52,9 +52,9 @@ class ResultLog(object): self.logfile = logfile # preferably line buffered def write_log_entry(self, testpath, lettercode, longrepr): - print_("%s %s" % (lettercode, testpath), file=self.logfile) + py.builtin.print_("%s %s" % (lettercode, testpath), file=self.logfile) for line in longrepr.splitlines(): - print_(" %s" % line, file=self.logfile) + py.builtin.print_(" %s" % line, file=self.logfile) def log_outcome(self, report, lettercode, longrepr): testpath = getattr(report, 'nodeid', None) --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -2,10 +2,10 @@ import py, pytest import os from _pytest.resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure -from _pytest.session import Node, Item, FSCollector +from _pytest.main import Node, Item, FSCollector def test_generic_path(testdir): - from _pytest.session import Session + from _pytest.main import Session config = testdir.parseconfig() session = Session(config) p1 = Node('a', config=config, session=session) --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -1,4 +1,4 @@ -Writing, managing and understanding plugins +Working with plugins and conftest files ============================================= .. _`local plugin`: @@ -11,6 +11,7 @@ py.test implements all aspects of config .. _`pytest/plugin`: http://bitbucket.org/hpk42/pytest/src/tip/pytest/plugin/ .. _`conftest.py plugins`: +.. _`conftest.py`: conftest.py: local per-directory plugins -------------------------------------------------------------- @@ -53,6 +54,7 @@ earlier than further away ones. conftest.py file. .. _`external plugins`: +.. _`extplugins`: Installing External Plugins / Searching ------------------------------------------------------ @@ -64,9 +66,26 @@ tool, for example:: pip uninstall pytest-NAME If a plugin is installed, py.test automatically finds and integrates it, -there is no need to activate it. If you don't need a plugin anymore simply -de-install it. You can find a list of available plugins through a -`pytest- pypi.python.org search`_. +there is no need to activate it. Here is a list of known plugins: + +* `pytest-capturelog `_: + to capture and assert about messages from the logging module + +* `pytest-xdist `_: + to distribute tests to CPUs and remote hosts, looponfailing mode, + see also :ref:`xdist` + +* `pytest-cov `_: + coverage reporting, compatible with distributed testing + +* `pytest-pep8 `_: + a ``--pep8`` option to enable PEP8 compliancy checking. + +* `oejskit `_: + a plugin to run javascript unittests in life browsers + (**version 0.8.9 not compatible with pytest-2.0**) + +You may discover more plugins through a `pytest- pypi.python.org search`_. .. _`available installable plugins`: .. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search @@ -170,12 +189,42 @@ the plugin manager like this: If you want to look at the names of existing plugins, use the ``--traceconfig`` option. +.. _`findpluginname`: + +Finding out which plugins are active +---------------------------------------------------------------------------- + +If you want to find out which plugins are active in your +environment you can type:: + + py.test --traceconfig + +and will get an extended test header which shows activated plugins +and their names. It will also print local plugins aka +:ref:`conftest.py ` files when they are loaded. + +.. _`cmdunregister`: + +deactivate / unregister a plugin by name +---------------------------------------------------------------------------- + +You can prevent plugins from loading or unregister them:: + + py.test -p no:NAME + +This means that any subsequent try to activate/load the named +plugin will it already existing. See :ref:`findpluginname` for +how to obtain the name of a plugin. .. _`builtin plugins`: py.test default plugin reference ==================================== + +You can find the source code for the following plugins +in the `pytest repository `_. + .. autosummary:: _pytest.assertion @@ -195,7 +244,7 @@ py.test default plugin reference _pytest.recwarn _pytest.resultlog _pytest.runner - _pytest.session + _pytest.main _pytest.skipping _pytest.terminal _pytest.tmpdir @@ -288,14 +337,14 @@ Reference of important objects involved .. autoclass:: _pytest.config.Parser :members: -.. autoclass:: _pytest.session.Node(name, parent) +.. autoclass:: _pytest.main.Node(name, parent) :members: .. - .. autoclass:: _pytest.session.File(fspath, parent) + .. autoclass:: _pytest.main.File(fspath, parent) :members: - .. autoclass:: _pytest.session.Item(name, parent) + .. autoclass:: _pytest.main.Item(name, parent) :members: .. autoclass:: _pytest.python.Module(name, parent) @@ -313,4 +362,3 @@ Reference of important objects involved .. autoclass:: _pytest.runner.TestReport :members: - --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -6,7 +6,7 @@ import re import inspect import time from fnmatch import fnmatch -from _pytest.session import Session +from _pytest.main import Session from py.builtin import print_ from _pytest.core import HookRelay --- a/_pytest/session.py +++ /dev/null @@ -1,516 +0,0 @@ -""" core implementation of testing process: init, session, runtest loop. """ - -import py -import pytest, _pytest -import os, sys -tracebackcutdir = py.path.local(_pytest.__file__).dirpath() - -# exitcodes for the command line -EXIT_OK = 0 -EXIT_TESTSFAILED = 1 -EXIT_INTERRUPTED = 2 -EXIT_INTERNALERROR = 3 - -def pytest_addoption(parser): - parser.addini("norecursedirs", "directory patterns to avoid for recursion", - type="args", default=('.*', 'CVS', '_darcs', '{arch}')) - #parser.addini("dirpatterns", - # "patterns specifying possible locations of test files", - # type="linelist", default=["**/test_*.txt", - # "**/test_*.py", "**/*_test.py"] - #) - group = parser.getgroup("general", "running and selection options") - group._addoption('-x', '--exitfirst', action="store_true", default=False, - dest="exitfirst", - help="exit instantly on first error or failed test."), - group._addoption('--maxfail', metavar="num", - action="store", type="int", dest="maxfail", default=0, - help="exit after first num failures or errors.") - - group = parser.getgroup("collect", "collection") - group.addoption('--collectonly', - action="store_true", dest="collectonly", - help="only collect tests, don't execute them."), - group.addoption('--pyargs', action="store_true", - help="try to interpret all arguments as python packages.") - group.addoption("--ignore", action="append", metavar="path", - help="ignore path during collection (multi-allowed).") - group.addoption('--confcutdir', dest="confcutdir", default=None, - metavar="dir", - help="only load conftest.py's relative to specified dir.") - - group = parser.getgroup("debugconfig", - "test process debugging and configuration") - group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", - help="base temporary directory for this test run.") - - -def pytest_namespace(): - return dict(collect=dict(Item=Item, Collector=Collector, File=File)) - -def pytest_configure(config): - py.test.config = config # compatibiltiy - if config.option.exitfirst: - config.option.maxfail = 1 - -def pytest_cmdline_main(config): - """ default command line protocol for initialization, session, - running tests and reporting. """ - session = Session(config) - session.exitstatus = EXIT_OK - try: - config.pluginmanager.do_configure(config) - config.hook.pytest_sessionstart(session=session) - config.hook.pytest_collection(session=session) - config.hook.pytest_runtestloop(session=session) - except pytest.UsageError: - raise - except KeyboardInterrupt: - excinfo = py.code.ExceptionInfo() - config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - session.exitstatus = EXIT_INTERRUPTED - except: - excinfo = py.code.ExceptionInfo() - config.pluginmanager.notify_exception(excinfo) - session.exitstatus = EXIT_INTERNALERROR - if excinfo.errisinstance(SystemExit): - sys.stderr.write("mainloop: caught Spurious SystemExit!\n") - if not session.exitstatus and session._testsfailed: - session.exitstatus = EXIT_TESTSFAILED - config.hook.pytest_sessionfinish(session=session, - exitstatus=session.exitstatus) - config.pluginmanager.do_unconfigure(config) - return session.exitstatus - -def pytest_collection(session): - session.perform_collect() - hook = session.config.hook - hook.pytest_collection_modifyitems(session=session, - config=session.config, items=session.items) - hook.pytest_collection_finish(session=session) - return True - -def pytest_runtestloop(session): - if session.config.option.collectonly: - return True - for item in session.session.items: - item.config.hook.pytest_runtest_protocol(item=item) - if session.shouldstop: - raise session.Interrupted(session.shouldstop) - return True - -def pytest_ignore_collect(path, config): - p = path.dirpath() - ignore_paths = config._getconftest_pathlist("collect_ignore", path=p) - ignore_paths = ignore_paths or [] - excludeopt = config.getvalue("ignore") - if excludeopt: - ignore_paths.extend([py.path.local(x) for x in excludeopt]) - return path in ignore_paths - -class HookProxy: - def __init__(self, fspath, config): - self.fspath = fspath - self.config = config - def __getattr__(self, name): - hookmethod = getattr(self.config.hook, name) - def call_matching_hooks(**kwargs): - plugins = self.config._getmatchingplugins(self.fspath) - return hookmethod.pcall(plugins, **kwargs) - return call_matching_hooks - -def compatproperty(name): - def fget(self): - #print "retrieving %r property from %s" %(name, self.fspath) - py.log._apiwarn("2.0", "use pytest.%s for " - "test collection and item classes" % name) - return getattr(pytest, name) - return property(fget, None, None, - "deprecated attribute %r, use pytest.%s" % (name,name)) - -class Node(object): - """ base class for all Nodes in the collection tree. - Collector subclasses have children, Items are terminal nodes.""" - - def __init__(self, name, parent=None, config=None, session=None): - #: a unique name with the scope of the parent - self.name = name - - #: the parent collector node. - self.parent = parent - - #: the test config object - self.config = config or parent.config - - #: the collection this node is part of - self.session = session or parent.session - - #: filesystem path where this node was collected from - self.fspath = getattr(parent, 'fspath', None) - self.ihook = self.session.gethookproxy(self.fspath) - self.keywords = {self.name: True} - - Module = compatproperty("Module") - Class = compatproperty("Class") - Function = compatproperty("Function") - File = compatproperty("File") - Item = compatproperty("Item") - - def __repr__(self): - return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) - - # methods for ordering nodes - @property - def nodeid(self): - try: - return self._nodeid - except AttributeError: - self._nodeid = x = self._makeid() - return x - - def _makeid(self): - return self.parent.nodeid + "::" + self.name - - def __eq__(self, other): - if not isinstance(other, Node): - return False - return self.__class__ == other.__class__ and \ - self.name == other.name and self.parent == other.parent - - def __ne__(self, other): - return not self == other - - def __hash__(self): - return hash((self.name, self.parent)) - - def setup(self): - pass - - def teardown(self): - pass - - def _memoizedcall(self, attrname, function): - exattrname = "_ex_" + attrname - failure = getattr(self, exattrname, None) - if failure is not None: - py.builtin._reraise(failure[0], failure[1], failure[2]) - if hasattr(self, attrname): - return getattr(self, attrname) - try: - res = function() - except py.builtin._sysex: - raise - except: - failure = py.std.sys.exc_info() - setattr(self, exattrname, failure) - raise - setattr(self, attrname, res) - return res - - def listchain(self): - """ return list of all parent collectors up to self, - starting from root of collection tree. """ - l = [self] - while 1: - x = l[0] - if x.parent is not None: # and x.parent.parent is not None: - l.insert(0, x.parent) - else: - return l - - def listnames(self): - return [x.name for x in self.listchain()] - - def getplugins(self): - return self.config._getmatchingplugins(self.fspath) - - def getparent(self, cls): - current = self - while current and not isinstance(current, cls): - current = current.parent - return current - - def _prunetraceback(self, excinfo): - pass - - def _repr_failure_py(self, excinfo, style=None): - if self.config.option.fulltrace: - style="long" - else: - self._prunetraceback(excinfo) - # XXX should excinfo.getrepr record all data and toterminal() - # process it? - if style is None: - if self.config.option.tbstyle == "short": - style = "short" - else: - style = "long" - return excinfo.getrepr(funcargs=True, - showlocals=self.config.option.showlocals, - style=style) - - repr_failure = _repr_failure_py - -class Collector(Node): - """ Collector instances create children through collect() - and thus iteratively build a tree. - """ - class CollectError(Exception): - """ an error during collection, contains a custom message. """ - - def collect(self): - """ returns a list of children (items and collectors) - for this collection node. - """ - raise NotImplementedError("abstract") - - def repr_failure(self, excinfo): - """ represent a collection failure. """ - if excinfo.errisinstance(self.CollectError): - exc = excinfo.value - return str(exc.args[0]) - return self._repr_failure_py(excinfo, style="short") - - def _memocollect(self): - """ internal helper method to cache results of calling collect(). """ - return self._memoizedcall('_collected', lambda: list(self.collect())) - - def _prunetraceback(self, excinfo): - if hasattr(self, 'fspath'): - path = self.fspath - traceback = excinfo.traceback - ntraceback = traceback.cut(path=self.fspath) - if ntraceback == traceback: - ntraceback = ntraceback.cut(excludepath=tracebackcutdir) - excinfo.traceback = ntraceback.filter() - -class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None, session=None): - fspath = py.path.local(fspath) # xxx only for test_resultlog.py? - name = fspath.basename - if parent is not None: - rel = fspath.relto(parent.fspath) - if rel: - name = rel - name = name.replace(os.sep, "/") - super(FSCollector, self).__init__(name, parent, config, session) - self.fspath = fspath - - def _makeid(self): - if self == self.session: - return "." - relpath = self.session.fspath.bestrelpath(self.fspath) - if os.sep != "/": - relpath = relpath.replace(os.sep, "/") - return relpath - -class File(FSCollector): - """ base class for collecting tests from a file. """ - -class Item(Node): - """ a basic test invocation item. Note that for a single function - there might be multiple test invocation items. - """ - def reportinfo(self): - return self.fspath, None, "" - - @property - def location(self): - try: - return self._location - except AttributeError: - location = self.reportinfo() - fspath = self.session.fspath.bestrelpath(location[0]) - location = (fspath, location[1], str(location[2])) - self._location = location - return location - -class NoMatch(Exception): - """ raised if matching cannot locate a matching names. """ - -class Session(FSCollector): - class Interrupted(KeyboardInterrupt): - """ signals an interrupted test run. """ - __module__ = 'builtins' # for py3 - - def __init__(self, config): - super(Session, self).__init__(py.path.local(), parent=None, - config=config, session=self) - self.config.pluginmanager.register(self, name="session", prepend=True) - self._testsfailed = 0 - self.shouldstop = False - self.trace = config.trace.root.get("collection") - self._norecursepatterns = config.getini("norecursedirs") - - def pytest_collectstart(self): - if self.shouldstop: - raise self.Interrupted(self.shouldstop) - - def pytest_runtest_logreport(self, report): - if report.failed and 'xfail' not in getattr(report, 'keywords', []): - self._testsfailed += 1 - maxfail = self.config.getvalue("maxfail") - if maxfail and self._testsfailed >= maxfail: - self.shouldstop = "stopping after %d failures" % ( - self._testsfailed) - pytest_collectreport = pytest_runtest_logreport - - def isinitpath(self, path): - return path in self._initialpaths - - def gethookproxy(self, fspath): - return HookProxy(fspath, self.config) - - def perform_collect(self, args=None, genitems=True): - if args is None: - args = self.config.args - self.trace("perform_collect", self, args) - self.trace.root.indent += 1 - self._notfound = [] - self._initialpaths = set() - self._initialparts = [] - for arg in args: - parts = self._parsearg(arg) - self._initialparts.append(parts) - self._initialpaths.add(parts[0]) - self.ihook.pytest_collectstart(collector=self) - rep = self.ihook.pytest_make_collect_report(collector=self) - self.ihook.pytest_collectreport(report=rep) - self.trace.root.indent -= 1 - if self._notfound: - for arg, exc in self._notfound: - line = "(no name %r in any of %r)" % (arg, exc.args[0]) - raise pytest.UsageError("not found: %s\n%s" %(arg, line)) - if not genitems: - return rep.result - else: - self.items = items = [] - if rep.passed: - for node in rep.result: - self.items.extend(self.genitems(node)) - return items - - def collect(self): - for parts in self._initialparts: - arg = "::".join(map(str, parts)) - self.trace("processing argument", arg) - self.trace.root.indent += 1 - try: - for x in self._collect(arg): - yield x - except NoMatch: - # we are inside a make_report hook so - # we cannot directly pass through the exception - self._notfound.append((arg, sys.exc_info()[1])) - self.trace.root.indent -= 1 - break - self.trace.root.indent -= 1 - - def _collect(self, arg): - names = self._parsearg(arg) - path = names.pop(0) - if path.check(dir=1): - assert not names, "invalid arg %r" %(arg,) - for path in path.visit(fil=lambda x: x.check(file=1), - rec=self._recurse, bf=True, sort=True): - for x in self._collectfile(path): - yield x - else: - assert path.check(file=1) - for x in self.matchnodes(self._collectfile(path), names): - yield x - - def _collectfile(self, path): - ihook = self.gethookproxy(path) - if not self.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): - return () - return ihook.pytest_collect_file(path=path, parent=self) - - def _recurse(self, path): - ihook = self.gethookproxy(path.dirpath()) - if ihook.pytest_ignore_collect(path=path, config=self.config): - return - for pat in self._norecursepatterns: - if path.check(fnmatch=pat): - return False - ihook = self.gethookproxy(path) - ihook.pytest_collect_directory(path=path, parent=self) - return True - - def _tryconvertpyarg(self, x): - try: - mod = __import__(x, None, None, ['__doc__']) - except (ValueError, ImportError): - return x - p = py.path.local(mod.__file__) - if p.purebasename == "__init__": - p = p.dirpath() - else: - p = p.new(basename=p.purebasename+".py") - return p - - def _parsearg(self, arg): - """ return (fspath, names) tuple after checking the file exists. """ - arg = str(arg) - if self.config.option.pyargs: - arg = self._tryconvertpyarg(arg) - parts = str(arg).split("::") - relpath = parts[0].replace("/", os.sep) - path = self.fspath.join(relpath, abs=True) - if not path.check(): - if self.config.option.pyargs: - msg = "file or package not found: " - else: - msg = "file not found: " - raise pytest.UsageError(msg + arg) - parts[0] = path - return parts - - def matchnodes(self, matching, names): - self.trace("matchnodes", matching, names) - self.trace.root.indent += 1 - nodes = self._matchnodes(matching, names) - num = len(nodes) - self.trace("matchnodes finished -> ", num, "nodes") - self.trace.root.indent -= 1 - if num == 0: - raise NoMatch(matching, names[:1]) - return nodes - - def _matchnodes(self, matching, names): - if not matching or not names: - return matching - name = names[0] - assert name - nextnames = names[1:] - resultnodes = [] - for node in matching: - if isinstance(node, pytest.Item): - if not names: - resultnodes.append(node) - continue - assert isinstance(node, pytest.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - if rep.passed: - for x in rep.result: - if x.name == name: - resultnodes.extend(self.matchnodes([x], nextnames)) - node.ihook.pytest_collectreport(report=rep) - return resultnodes - - def genitems(self, node): - self.trace("genitems", node) - if isinstance(node, pytest.Item): - node.ihook.pytest_itemcollected(item=node) - yield node - else: - assert isinstance(node, pytest.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - if rep.passed: - for subnode in rep.result: - for x in self.genitems(subnode): - yield x - node.ihook.pytest_collectreport(report=rep) --- a/testing/test_config.py +++ b/testing/test_config.py @@ -219,7 +219,7 @@ def test_options_on_small_file_do_not_bl ['--traceconfig'], ['-v'], ['-v', '-v']): runfiletest(opts + [path]) -def test_preparse_ordering(testdir, monkeypatch): +def test_preparse_ordering_with_setuptools(testdir, monkeypatch): pkg_resources = py.test.importorskip("pkg_resources") def my_iter(name): assert name == "pytest11" @@ -239,3 +239,16 @@ def test_preparse_ordering(testdir, monk plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 +def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch): + pkg_resources = py.test.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "mytestplugin" + def load(self): + assert 0, "should not arrive here" + return iter([EntryPoint()]) + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + config = testdir.parseconfig("-p", "no:mytestplugin") + plugin = config.pluginmanager.getplugin("mytestplugin") + assert plugin == -1 --- a/tox.ini +++ b/tox.ini @@ -68,7 +68,7 @@ commands= [pytest] minversion=2.0 plugins=pytester -addopts= -rxf --pyargs --doctest-modules --ignore=.tox +#addopts= -rxf --pyargs --doctest-modules --ignore=.tox rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py python_classes=Test Acceptance --- a/_pytest/config.py +++ b/_pytest/config.py @@ -300,9 +300,9 @@ class Config(object): if addopts: args[:] = self.getini("addopts") + args self._checkversion() + self.pluginmanager.consider_preparse(args) self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() - self.pluginmanager.consider_preparse(args) self._setinitialconftest(args) self.pluginmanager.do_addoption(self._parser) --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,6 @@ import pytest, py -from _pytest.session import Session +from _pytest.main import Session class TestCollector: def test_collect_versus_item(self): From commits-noreply at bitbucket.org Mon Dec 6 18:24:17 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 6 Dec 2010 11:24:17 -0600 (CST) Subject: [py-svn] pytest commit 3b67bd77f30b: fix issue9 wrong XPass with failing setup/teardown function of xfail marked test Message-ID: <20101206172417.2D5931E135E@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291656047 -3600 # Node ID 3b67bd77f30b91a56847d5a565f80e74979443ee # Parent 1c3eb86502b30a52e8adf798967b7d547edd8f43 fix issue9 wrong XPass with failing setup/teardown function of xfail marked test now when setup or teardown of a test item/function fails and the test is marked "xfail" it will show up as an xfail-ed test. --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -97,19 +97,19 @@ def pytest_runtest_makereport(__multical rep.keywords['xfail'] = "reason: " + call.excinfo.value.msg rep.outcome = "skipped" return rep - if call.when == "call": - rep = __multicall__.execute() - evalxfail = getattr(item, '_evalxfail') - if not item.config.getvalue("runxfail") and evalxfail.istrue(): - if call.excinfo: - rep.outcome = "skipped" - else: - rep.outcome = "failed" + rep = __multicall__.execute() + evalxfail = item._evalxfail + if not item.config.option.runxfail and evalxfail.istrue(): + if call.excinfo: + rep.outcome = "skipped" rep.keywords['xfail'] = evalxfail.getexplanation() - else: - if 'xfail' in rep.keywords: - del rep.keywords['xfail'] - return rep + elif call.when == "call": + rep.outcome = "failed" + rep.keywords['xfail'] = evalxfail.getexplanation() + else: + if 'xfail' in rep.keywords: + del rep.keywords['xfail'] + return rep # called by terminalreporter progress reporting def pytest_report_teststatus(report): --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -308,6 +308,37 @@ class TestXFail: "*1 xfailed*", ]) +class TestXFailwithSetupTeardown: + def test_failing_setup_issue9(self, testdir): + testdir.makepyfile(""" + import pytest + def setup_function(func): + assert 0 + + @pytest.mark.xfail + def test_func(): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*1 xfail*", + ]) + + def test_failing_teardown_issue9(self, testdir): + testdir.makepyfile(""" + import pytest + def teardown_function(func): + assert 0 + + @pytest.mark.xfail + def test_func(): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*1 xfail*", + ]) + class TestSkipif: def test_skipif_conditional(self, testdir): --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- +- fix issue9: direct setup/teardown functions for an xfail-marked + test will report as xfail if they fail (but reported as normal + passing setup/teardown). - fix issue8: no logging errors at process exit - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output From commits-noreply at bitbucket.org Mon Dec 6 18:32:11 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 6 Dec 2010 11:32:11 -0600 (CST) Subject: [py-svn] pytest commit f9e54a46a0e8: fix hasplugin() method / test failures Message-ID: <20101206173211.25C216C1493@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291656724 -3600 # Node ID f9e54a46a0e8f53550a56927d5b9e00acf7ea310 # Parent 3b67bd77f30b91a56847d5a565f80e74979443ee fix hasplugin() method / test failures --- a/_pytest/core.py +++ b/_pytest/core.py @@ -119,11 +119,7 @@ class PluginManager(object): py.test.skip("plugin %r is missing" % name) def hasplugin(self, name): - try: - self.getplugin(name) - return True - except KeyError: - return False + return bool(self.getplugin(name)) def getplugin(self, name): if name is None: --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,11 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- -- fix issue9: direct setup/teardown functions for an xfail-marked - test will report as xfail if they fail (but reported as normal - passing setup/teardown). +- fix issue9: setup/teardown functions for an xfail-marked + test will report as xfail if they fail but report as normally + passing (not xpassing) if they succeed. This only is true + for "direct" setup/teardown invocations because teardown_class/ + teardown_module cannot closely relate to a single test. - fix issue8: no logging errors at process exit - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output From commits-noreply at bitbucket.org Mon Dec 6 19:00:46 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 6 Dec 2010 12:00:46 -0600 (CST) Subject: [py-svn] pytest commit ea8c9c761f10: fix issue7 - assert failure inside doctest doesn't prettyprint Message-ID: <20101206180046.B6CE424132C@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291658430 -3600 # Node ID ea8c9c761f10bff419ff3b4472d6472e2c23f14f # Parent f9e54a46a0e8f53550a56927d5b9e00acf7ea310 fix issue7 - assert failure inside doctest doesn't prettyprint unexpected exceptions are now reported within the doctest failure representation context. --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -48,19 +48,16 @@ class TestDoctests: def test_doctest_unexpected_exception(self, testdir): p = testdir.maketxtfile(""" >>> i = 0 - >>> i = 1 - >>> x + >>> 0 / i 2 """) - reprec = testdir.inline_run(p) - call = reprec.getcall("pytest_runtest_logreport") - assert call.report.failed - assert call.report.longrepr - # XXX - #testitem, = items - #excinfo = pytest.raises(Failed, "testitem.runtest()") - #repr = testitem.repr_failure(excinfo, ("", "")) - #assert repr.reprlocation + result = testdir.runpytest("--doctest-modules") + result.stdout.fnmatch_lines([ + "*unexpected_exception*", + "*>>> i = 0*", + "*>>> 0 / i*", + "*UNEXPECTED*ZeroDivision*", + ]) def test_doctestmodule(self, testdir): p = testdir.makepyfile(""" --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -34,7 +34,9 @@ class ReprFailDoctest(TerminalRepr): class DoctestItem(pytest.Item): def repr_failure(self, excinfo): - if excinfo.errisinstance(py.std.doctest.DocTestFailure): + doctest = py.std.doctest + if excinfo.errisinstance((doctest.DocTestFailure, + doctest.UnexpectedException)): doctestfailure = excinfo.value example = doctestfailure.example test = doctestfailure.test @@ -50,12 +52,15 @@ class DoctestItem(pytest.Item): for line in filelines[i:lineno]: lines.append("%03d %s" % (i+1, line)) i += 1 - lines += checker.output_difference(example, - doctestfailure.got, REPORT_UDIFF).split("\n") + if excinfo.errisinstance(doctest.DocTestFailure): + lines += checker.output_difference(example, + doctestfailure.got, REPORT_UDIFF).split("\n") + else: + inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) + lines += ["UNEXPECTED EXCEPTION: %s" % + repr(inner_excinfo.value)] + return ReprFailDoctest(reprlocation, lines) - elif excinfo.errisinstance(py.std.doctest.UnexpectedException): - excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) - return super(DoctestItem, self).repr_failure(excinfo) else: return super(DoctestItem, self).repr_failure(excinfo) --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- +- fix issue10: assert failures in doctest modules. + unexpected failures in doctests will not generally + show nicer, i.e. within the doctest failing context. - fix issue9: setup/teardown functions for an xfail-marked test will report as xfail if they fail but report as normally passing (not xpassing) if they succeed. This only is true From commits-noreply at bitbucket.org Mon Dec 6 19:02:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 6 Dec 2010 12:02:02 -0600 (CST) Subject: [py-svn] pytest commit 1f77064f9725: bump version and fix changelog issue reference Message-ID: <20101206180202.07C496C148A@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291658510 -3600 # Node ID 1f77064f972513986fcc53bbf31a789ec032272f # Parent ea8c9c761f10bff419ff3b4472d6472e2c23f14f bump version and fix changelog issue reference --- a/pytest.py +++ b/pytest.py @@ -1,7 +1,7 @@ """ unit and functional testing with Python. """ -__version__ = '2.0.1.dev1' +__version__ = '2.0.1.dev3' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.1.dev1', + version='2.0.1.dev3', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,7 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- -- fix issue10: assert failures in doctest modules. +- fix issue7: assert failures in doctest modules. unexpected failures in doctests will not generally show nicer, i.e. within the doctest failing context. - fix issue9: setup/teardown functions for an xfail-marked From commits-noreply at bitbucket.org Tue Dec 7 12:01:47 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 7 Dec 2010 05:01:47 -0600 (CST) Subject: [py-svn] pytest-xdist commit c2e2cfb63950: fix and shorten collection reporting and fix a test wrt internal pytest api Message-ID: <20101207110147.BE1811E0FAC@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1291719622 -3600 # Node ID c2e2cfb63950535df48a7cdf269fba18c5e33f76 # Parent 94be523cbf10d0df4ae903ba9b575b4ed6c5725a fix and shorten collection reporting and fix a test wrt internal pytest api --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -62,7 +62,7 @@ def test_remoteinitconfig(testdir): config1 = testdir.parseconfig() config2 = remote_initconfig(config1.option.__dict__, config1.args) assert config2.option.__dict__ == config1.option.__dict__ - py.test.raises(KeyError, 'config2.pluginmanager.getplugin("terminal")') + assert config2.pluginmanager.getplugin("terminal") == None class TestReportSerialization: def test_itemreport_outcomes(self, testdir): --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5', + version='1.6', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=open('README.txt').read(), license='GPLv2 or later', --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -267,6 +267,7 @@ class DSession: if self.sched.collection_is_completed: if self.terminal: + self.trdist.ensure_show_status() self.terminal.write_line("") self.terminal.write_line("scheduling tests via %s" %( self.sched.__class__.__name__)) @@ -327,13 +328,19 @@ class TerminalDistReporter: def write_line(self, msg): self.tr.write_line(msg) + def ensure_show_status(self): + if not self.tr.hasmarkup: + self.write_line(self.getstatus()) + def setstatus(self, spec, status, show=True): self._status[spec.id] = status - if show: - parts = ["%s %s" %(spec.id, self._status[spec.id]) - for spec in self._specs] - line = " / ".join(parts) - self.rewrite(line) + if show and self.tr.hasmarkup: + self.rewrite(self.getstatus()) + + def getstatus(self): + parts = ["%s %s" %(spec.id, self._status[spec.id]) + for spec in self._specs] + return " / ".join(parts) def rewrite(self, line, newline=False): pline = line + " " * max(self._lastlen-len(line), 0) @@ -347,8 +354,9 @@ class TerminalDistReporter: def pytest_xdist_setupnodes(self, specs): self._specs = specs for spec in specs: - self.setstatus(spec, "initializing", show=False) - self.setstatus(spec, "initializing", show=True) + self.setstatus(spec, "I", show=False) + self.setstatus(spec, "I", show=True) + self.ensure_show_status() def pytest_xdist_newgateway(self, gateway): if self.config.option.verbose > 0: @@ -357,7 +365,7 @@ class TerminalDistReporter: self.rewrite("[%s] %s Python %s cwd: %s" % ( gateway.id, rinfo.platform, version, rinfo.cwd), newline=True) - self.setstatus(gateway.spec, "collecting") + self.setstatus(gateway.spec, "C") def pytest_testnodeready(self, node): if self.config.option.verbose > 0: @@ -366,7 +374,7 @@ class TerminalDistReporter: d['id'], d['version'].replace('\n', ' -- '),) self.rewrite(infoline, newline=True) - self.setstatus(node.gateway.spec, "ready") + self.setstatus(node.gateway.spec, "ok") def pytest_testnodedown(self, node, error): if not error: --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5' +__version__ = '1.6' From commits-noreply at bitbucket.org Tue Dec 7 12:39:52 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 7 Dec 2010 05:39:52 -0600 (CST) Subject: [py-svn] pytest commit d796239b64d8: introduce a pytest_cmdline_processargs hook to modify/add dynamically to command line arguments. Message-ID: <20101207113952.DDE5324121E@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291720452 -3600 # Node ID d796239b64d87e078d33d4beda73ac8d4509abc5 # Parent 1f77064f972513986fcc53bbf31a789ec032272f introduce a pytest_cmdline_processargs hook to modify/add dynamically to command line arguments. --- a/doc/Makefile +++ b/doc/Makefile @@ -40,7 +40,7 @@ clean: -rm -rf $(BUILDDIR)/* install: clean html - rsync -avz _build/html/ code:www-pytest/ + rsync -avz _build/html/ code:www-pytest/2.0.1dev html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -138,11 +138,42 @@ let's run the full monty:: E assert 4 < 4 test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.03 seconds + 1 failed, 4 passed in 0.02 seconds As expected when running the full range of ``param1`` values we'll get an error on the last one. +dynamically adding command line options +-------------------------------------------------------------- + +.. regendoc:wipe + +Through :confval:`addopts` you can statically add command line +options for your project. You can also dynamically modify +the command line arguments before they get processed:: + + # content of conftest.py + import sys + def pytest_cmdline_processargs(args): + if 'xdist' in sys.modules: # pytest-xdist plugin + import multiprocessing + num = max(multiprocessing.cpu_count() / 2, 1) + args[:] = ["-n", str(num)] + args + +If you have the :ref:`xdist plugin ` installed +you will now always perform test runs using a number +of subprocesses close to your CPU. Running in an empty +directory with the above conftest.py:: + + $ py.test + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.1.dev3 + gw0 I / gw1 I / gw2 I / gw3 I + gw0 [0] / gw1 [0] / gw2 [0] / gw3 [0] + + scheduling tests via LoadScheduling + + ============================= in 0.29 seconds ============================= .. _`retrieved by hooks as item keywords`: @@ -183,12 +214,12 @@ and when running it will see a skipped " $ py.test -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.1.dev3 collecting ... collected 2 items test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-479/conftest.py:9: need --runslow option to run + SKIP [1] /tmp/doc-exec-25/conftest.py:9: need --runslow option to run =================== 1 passed, 1 skipped in 0.02 seconds ==================== @@ -196,7 +227,7 @@ Or run it including the ``slow`` marked $ py.test --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.1.dev3 collecting ... collected 2 items test_module.py .. --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- +- introduce a pytest_cmdline_processargs(args) hook + to allow dynamic computation of command line arguments. + This fixes a regression because py.test prior to 2.0 + allowed to set command line options from conftest.py + files which so far pytest-2.0 only allowed from ini-files now. - fix issue7: assert failures in doctest modules. unexpected failures in doctests will not generally show nicer, i.e. within the doctest failing context. --- a/testing/test_config.py +++ b/testing/test_config.py @@ -82,7 +82,6 @@ class TestConfigCmdlineParsing: class TestConfigAPI: - def test_config_trace(self, testdir): config = testdir.Config() l = [] @@ -252,3 +251,14 @@ def test_plugin_preparse_prevents_setupt config = testdir.parseconfig("-p", "no:mytestplugin") plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin == -1 + +def test_cmdline_processargs_simple(testdir): + testdir.makeconftest(""" + def pytest_cmdline_processargs(args): + args.append("-h") + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*pytest*", + "*-h*", + ]) --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -19,6 +19,9 @@ def pytest_cmdline_parse(pluginmanager, """return initialized config object, parsing the specified args. """ pytest_cmdline_parse.firstresult = True +def pytest_cmdline_processargs(config, args): + """modify command line arguments before option parsing. """ + def pytest_addoption(parser): """add optparse-style options and ini-style config values via calls to ``parser.addoption`` and ``parser.addini(...)``. @@ -202,7 +205,6 @@ def pytest_doctest_prepare_content(conte """ return processed content for a given doctest""" pytest_doctest_prepare_content.firstresult = True - # ------------------------------------------------------------------------- # error handling and internal debugging hooks # ------------------------------------------------------------------------- --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -271,6 +271,7 @@ initialisation, command line and configu .. currentmodule:: _pytest.hookspec +.. autofunction:: pytest_cmdline_processargs .. autofunction:: pytest_cmdline_parse .. autofunction:: pytest_namespace .. autofunction:: pytest_addoption --- a/_pytest/config.py +++ b/_pytest/config.py @@ -305,6 +305,8 @@ class Config(object): self.pluginmanager.consider_env() self._setinitialconftest(args) self.pluginmanager.do_addoption(self._parser) + if addopts: + self.hook.pytest_cmdline_processargs(config=self, args=args) def _checkversion(self): minver = self.inicfg.get('minversion', None) From commits-noreply at bitbucket.org Tue Dec 7 12:39:52 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 7 Dec 2010 05:39:52 -0600 (CST) Subject: [py-svn] pytest commit 031d0af75303: make getvalueorskip() be hidden in skip-reporting. also bump version. Message-ID: <20101207113952.EDE5C24123C@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291720704 -3600 # Node ID 031d0af75303e3bf6af99b1dcea784c113369083 # Parent d796239b64d87e078d33d4beda73ac8d4509abc5 make getvalueorskip() be hidden in skip-reporting. also bump version. --- a/pytest.py +++ b/pytest.py @@ -1,7 +1,7 @@ """ unit and functional testing with Python. """ -__version__ = '2.0.1.dev3' +__version__ = '2.0.1.dev4' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.1.dev3', + version='2.0.1.dev4', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/_pytest/config.py +++ b/_pytest/config.py @@ -399,7 +399,9 @@ class Config(object): return self._getconftest(name, path, check=False) def getvalueorskip(self, name, path=None): - """ (deprecated) return getvalue(name) or call py.test.skip if no value exists. """ + """ (deprecated) return getvalue(name) or call + py.test.skip if no value exists. """ + __tracebackhide__ = True try: val = self.getvalue(name, path) if val is None: --- a/testing/test_config.py +++ b/testing/test_config.py @@ -111,8 +111,15 @@ class TestConfigAPI: verbose = config.getvalueorskip("verbose") assert verbose == config.option.verbose config.option.hello = None - pytest.raises(pytest.skip.Exception, - "config.getvalueorskip('hello')") + try: + config.getvalueorskip('hello') + except KeyboardInterrupt: + raise + except: + excinfo = py.code.ExceptionInfo() + frame = excinfo.traceback[-2].frame + assert frame.code.name == "getvalueorskip" + assert frame.eval("__tracebackhide__") def test_config_overwrite(self, testdir): o = testdir.tmpdir From commits-noreply at bitbucket.org Tue Dec 7 12:39:53 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 7 Dec 2010 05:39:53 -0600 (CST) Subject: [py-svn] pytest commit febbfb4066a2: rather named the new hook cmdline_preparse Message-ID: <20101207113953.0E5A7241344@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291721658 -3600 # Node ID febbfb4066a2e909d993ab24123f5f4e7f35fe4d # Parent 031d0af75303e3bf6af99b1dcea784c113369083 rather named the new hook cmdline_preparse --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -154,7 +154,7 @@ the command line arguments before they g # content of conftest.py import sys - def pytest_cmdline_processargs(args): + def pytest_cmdline_preparse(args): if 'xdist' in sys.modules: # pytest-xdist plugin import multiprocessing num = max(multiprocessing.cpu_count() / 2, 1) --- a/testing/test_config.py +++ b/testing/test_config.py @@ -261,7 +261,7 @@ def test_plugin_preparse_prevents_setupt def test_cmdline_processargs_simple(testdir): testdir.makeconftest(""" - def pytest_cmdline_processargs(args): + def pytest_cmdline_preparse(args): args.append("-h") """) result = testdir.runpytest() --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -19,7 +19,7 @@ def pytest_cmdline_parse(pluginmanager, """return initialized config object, parsing the specified args. """ pytest_cmdline_parse.firstresult = True -def pytest_cmdline_processargs(config, args): +def pytest_cmdline_preparse(config, args): """modify command line arguments before option parsing. """ def pytest_addoption(parser): --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -271,7 +271,7 @@ initialisation, command line and configu .. currentmodule:: _pytest.hookspec -.. autofunction:: pytest_cmdline_processargs +.. autofunction:: pytest_cmdline_preparse .. autofunction:: pytest_cmdline_parse .. autofunction:: pytest_namespace .. autofunction:: pytest_addoption --- a/_pytest/config.py +++ b/_pytest/config.py @@ -306,7 +306,7 @@ class Config(object): self._setinitialconftest(args) self.pluginmanager.do_addoption(self._parser) if addopts: - self.hook.pytest_cmdline_processargs(config=self, args=args) + self.hook.pytest_cmdline_preparse(config=self, args=args) def _checkversion(self): minver = self.inicfg.get('minversion', None) --- a/pytest.py +++ b/pytest.py @@ -1,7 +1,7 @@ """ unit and functional testing with Python. """ -__version__ = '2.0.1.dev4' +__version__ = '2.0.1.dev5' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.1.dev4', + version='2.0.1.dev5', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From commits-noreply at bitbucket.org Tue Dec 7 12:47:47 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 7 Dec 2010 05:47:47 -0600 (CST) Subject: [py-svn] pytest-xdist commit 3ec35b3bbec1: fix test Message-ID: <20101207114747.0B5E06C14C2@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1291722456 -3600 # Node ID 3ec35b3bbec16f87f7b6f26243aecd7284a75dae # Parent 57e9dab307bda0f3ffbf4f12b3177fc0f861968f fix test --- a/testing/test_dsession.py +++ b/testing/test_dsession.py @@ -1,5 +1,5 @@ from xdist.dsession import DSession, LoadScheduling, EachScheduling -from _pytest import session as outcome +from _pytest import main as outcome import py import execnet From commits-noreply at bitbucket.org Tue Dec 7 12:47:46 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 7 Dec 2010 05:47:46 -0600 (CST) Subject: [py-svn] pytest-xdist commit 57e9dab307bd: bump version Message-ID: <20101207114746.D92BD6C14B9@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1291721925 -3600 # Node ID 57e9dab307bda0f3ffbf4f12b3177fc0f861968f # Parent c2e2cfb63950535df48a7cdf269fba18c5e33f76 bump version --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.6', + version='1.6.dev2', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=open('README.txt').read(), license='GPLv2 or later', --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.6' +__version__ = '1.6.dev2' From commits-noreply at bitbucket.org Tue Dec 7 15:25:38 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 7 Dec 2010 08:25:38 -0600 (CST) Subject: [py-svn] pytest commit 33139ed32f74: fix issue11 doc typo (thanks Eduardo) Message-ID: <20101207142538.A4C3C6C14B9@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291731925 -3600 # Node ID 33139ed32f742292e68b2d69976164feb6703d0f # Parent febbfb4066a2e909d993ab24123f5f4e7f35fe4d fix issue11 doc typo (thanks Eduardo) --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -14,7 +14,7 @@ Dependency injection through function ar py.test allows to inject values into test functions through the *funcarg mechanism*: For each argument name in a test function signature a factory is looked up and called to create the value. The factory can live in the -same test class, test module, in a per-directory ``confest.py`` file or +same test class, test module, in a per-directory ``conftest.py`` file or in an external plugin. It has full access to the requesting test function, can register finalizers and invoke lifecycle-caching helpers. As can be expected from a systematic dependency From commits-noreply at bitbucket.org Thu Dec 9 11:05:43 2010 From: commits-noreply at bitbucket.org (Bitbucket) Date: Thu, 9 Dec 2010 04:05:43 -0600 (CST) Subject: [py-svn] pytest commit 7bca4ef2da85: check docstring at test time instead of runtime, improve and test warning on assertion turned off (thanks FND for reporting) Message-ID: <20101209100543.1D6C71E0FB2@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291888831 -3600 # Node ID 7bca4ef2da85c138c72d21d279aa921b7d2cbcb0 # Parent 33139ed32f742292e68b2d69976164feb6703d0f check docstring at test time instead of runtime, improve and test warning on assertion turned off (thanks FND for reporting) --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -201,3 +201,14 @@ def test_traceback_failure(testdir): "*test_traceback_failure.py:4: AssertionError" ]) + at pytest.mark.skipif("sys.version_info < (2,5)") +def test_warn_missing(testdir): + p1 = testdir.makepyfile("") + result = testdir.run(sys.executable, "-OO", "-m", "pytest", "-h") + result.stderr.fnmatch_lines([ + "*WARNING*assertion*", + ]) + result = testdir.run(sys.executable, "-OO", "-m", "pytest", "--no-assert") + result.stderr.fnmatch_lines([ + "*WARNING*assertion*", + ]) --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -218,6 +218,12 @@ class TestGeneralUsage: assert res.ret res.stderr.fnmatch_lines(["*ERROR*not found*"]) + def test_docstring_on_hookspec(self): + from _pytest import hookspec + for name, value in vars(hookspec).items(): + if name.startswith("pytest_"): + assert value.__doc__, "no docstring for %s" % name + class TestInvocationVariants: def test_earlyinit(self, testdir): p = testdir.makepyfile(""" --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- +- improve behaviour/warnings when running on top of "python -OO" + (assertions and docstrings are turned off, leading to potential + false positives) - introduce a pytest_cmdline_processargs(args) hook to allow dynamic computation of command line arguments. This fixes a regression because py.test prior to 2.0 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -362,9 +362,6 @@ class HookRelay: added = False for name, method in vars(hookspecs).items(): if name.startswith(prefix): - if not method.__doc__: - raise ValueError("docstring required for hook %r, in %r" - % (method, hookspecs)) firstresult = getattr(method, 'firstresult', False) hc = HookCaller(self, name, firstresult=firstresult) setattr(self, name, hc) --- a/testing/test_core.py +++ b/testing/test_core.py @@ -319,25 +319,6 @@ class TestPytestPluginInteractions: res = config.hook.pytest_myhook(xyz=10) assert res == [11] - def test_addhooks_docstring_error(self, testdir): - newhooks = testdir.makepyfile(newhooks=""" - class A: # no pytest_ prefix - pass - def pytest_myhook(xyz): - pass - """) - conf = testdir.makeconftest(""" - import sys ; sys.path.insert(0, '.') - import newhooks - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(newhooks) - """) - res = testdir.runpytest() - assert res.ret != 0 - res.stderr.fnmatch_lines([ - "*docstring*pytest_myhook*newhooks*" - ]) - def test_addhooks_nohooks(self, testdir): conf = testdir.makeconftest(""" import sys --- a/_pytest/assertion.py +++ b/_pytest/assertion.py @@ -17,8 +17,8 @@ def pytest_configure(config): # turn call the hooks defined here as part of the # DebugInterpreter. config._monkeypatch = m = monkeypatch() + warn_about_missing_assertion() if not config.getvalue("noassert") and not config.getvalue("nomagic"): - warn_about_missing_assertion() def callbinrepr(op, left, right): hook_result = config.hook.pytest_assertrepr_compare( config=config, op=op, left=left, right=right) @@ -38,9 +38,8 @@ def warn_about_missing_assertion(): except AssertionError: pass else: - py.std.warnings.warn("Assertions are turned off!" - " (are you using python -O?)") - + sys.stderr.write("WARNING: failing tests may report as passing because " + "assertions are turned off! (are you using python -O?)\n") # Provide basestring in python3 try: From commits-noreply at bitbucket.org Fri Dec 10 12:48:15 2010 From: commits-noreply at bitbucket.org (Bitbucket) Date: Fri, 10 Dec 2010 05:48:15 -0600 (CST) Subject: [py-svn] pytest commit 41243ce59fdb: Add "not in" to detailed explanations Message-ID: <20101210114815.B7C6D241085@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Floris Bruynooghe # Date 1291943006 0 # Node ID 41243ce59fdb9396360258cf309af30737e1d6b2 # Parent 7bca4ef2da85c138c72d21d279aa921b7d2cbcb0 Add "not in" to detailed explanations This simply uses difflib to compare the text without the offending string to the full text. Also ensures the summary line uses all space available. But the terminal width is still hardcoded. --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -114,6 +114,11 @@ class TestAssert_reprcompare: expl = callequal(A(), '') assert not expl +def test_reprcompare_notin(): + detail = plugin.pytest_assertrepr_compare('not in', 'foo', 'aaafoobbb')[1:] + assert '- aaabbb' in detail + assert '+ aaafoobbb' in detail + @needsnewassert def test_pytest_assertrepr_compare_integration(testdir): testdir.makepyfile(""" --- a/_pytest/assertion.py +++ b/_pytest/assertion.py @@ -50,8 +50,9 @@ except NameError: def pytest_assertrepr_compare(op, left, right): """return specialised explanations for some operators/operands""" - left_repr = py.io.saferepr(left, maxsize=30) - right_repr = py.io.saferepr(right, maxsize=30) + width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op + left_repr = py.io.saferepr(left, maxsize=width/2) + right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) summary = '%s %s %s' % (left_repr, op, right_repr) issequence = lambda x: isinstance(x, (list, tuple)) @@ -71,6 +72,9 @@ def pytest_assertrepr_compare(op, left, elif isdict(left) and isdict(right): explanation = _diff_text(py.std.pprint.pformat(left), py.std.pprint.pformat(right)) + elif op == 'not in': + if istext(left) and istext(right): + explanation = _notin_text(left, right) except py.builtin._sysex: raise except: @@ -154,3 +158,11 @@ def _compare_eq_set(left, right): for item in diff_right: explanation.append(py.io.saferepr(item)) return explanation + + +def _notin_text(term, text): + index = text.find(term) + head = text[:index] + tail = text[index+len(term):] + correct_text = head + tail + return _diff_text(correct_text, text) --- a/doc/example/assertion/failure_demo.py +++ b/doc/example/assertion/failure_demo.py @@ -77,6 +77,22 @@ class TestSpecialisedExplanations(object def test_in_list(self): assert 1 in [0, 2, 3, 4, 5] + def test_not_in_text_multiline(self): + text = 'some multiline\ntext\nwhich\nincludes foo\nand a\ntail' + assert 'foo' not in text + + def test_not_in_text_single(self): + text = 'single foo line' + assert 'foo' not in text + + def test_not_in_text_single_long(self): + text = 'head ' * 50 + 'foo ' + 'tail ' * 20 + assert 'foo' not in text + + def test_not_in_text_single_long_term(self): + text = 'head ' * 50 + 'f'*70 + 'tail ' * 20 + assert 'f'*70 not in text + def test_attribute(): class Foo(object): From commits-noreply at bitbucket.org Fri Dec 10 12:48:15 2010 From: commits-noreply at bitbucket.org (Bitbucket) Date: Fri, 10 Dec 2010 05:48:15 -0600 (CST) Subject: [py-svn] pytest commit 2f8fa67e5883: small improvements, add assertion improvement to CHANGELOG Message-ID: <20101210114815.C83902412ED@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1291980484 -3600 # Node ID 2f8fa67e58831ae53db736b39909efd91e6b8f6f # Parent 41243ce59fdb9396360258cf309af30737e1d6b2 small improvements, add assertion improvement to CHANGELOG --- a/doc/example/assertion/test_failures.py +++ b/doc/example/assertion/test_failures.py @@ -9,6 +9,6 @@ def test_failure_demo_fails_properly(tes failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) result = testdir.runpytest(target) result.stdout.fnmatch_lines([ - "*35 failed*" + "*39 failed*" ]) assert result.ret != 0 --- a/doc/projects.txt +++ b/doc/projects.txt @@ -45,6 +45,7 @@ Some organisations using py.test * `Tandberg `_ * `Shootq `_ * `Stups department of Heinrich Heine University D??sseldorf `_ +* `cellzome`_ `_ * `Open End, Gotenborg `_ * `Laboraratory of Bioinformatics, Warsaw `_ * `merlinux, Germany `_ --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ -Changes between 2.0.0 and 2.0.1.dev1 +Changes between 2.0.0 and 2.0.1.devX ---------------------------------------------- +- improve assertions ("not in"), thanks Floris - improve behaviour/warnings when running on top of "python -OO" (assertions and docstrings are turned off, leading to potential false positives) From py-svn at codespeak.net Sun Dec 12 19:44:52 2010 From: py-svn at codespeak.net (py-svn at codespeak.net) Date: Sun, 12 Dec 2010 19:44:52 +0100 (CET) Subject: [py-svn] py-svn@codespeak.net 42% OFF on Pfizer! Message-ID: <20101212184452.0E5A1282B90@codespeak.net> An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Thu Dec 23 21:55:49 2010 From: commits-noreply at bitbucket.org (Bitbucket) Date: Thu, 23 Dec 2010 20:55:49 -0000 Subject: [py-svn] commit/pytest: gutworth: duplicate word Message-ID: <20101223205549.6718.96323@207-223-240-189.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/a3c478bff203/ changeset: r2131:a3c478bff203 user: gutworth date: 2010-12-23 21:56:38 summary: duplicate word affected #: 1 file (6 bytes) --- a/doc/tmpdir.txt Fri Dec 10 12:28:04 2010 +0100 +++ b/doc/tmpdir.txt Thu Dec 23 14:56:38 2010 -0600 @@ -64,8 +64,8 @@ py.test --basetemp=mydir When distributing tests on the local machine, ``py.test`` takes care to -configure a basetemp directory for the sub processes such that all -temporary data lands below below a single per-test run basetemp directory. +configure a basetemp directory for the sub processes such that all temporary +data lands below a single per-test run basetemp directory. .. _`py.path.local`: http://pylib.org/path.html 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.