From 9e6bb74d710179163fc4f61de70183da0bf636a2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 23 Jan 2016 18:08:18 +0100 Subject: [PATCH 1/8] reformat monkeypatch core plugin/its tests --- _pytest/monkeypatch.py | 14 +++++++------- testing/test_monkeypatch.py | 32 +++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 5f9720f1f..97aa67c6c 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -5,7 +5,6 @@ import re from py.builtin import _basestring - RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") @@ -32,7 +31,6 @@ def pytest_funcarg__monkeypatch(request): return mpatch - def derive_importpath(import_path, raising): import pytest if not isinstance(import_path, _basestring) or "." not in import_path: @@ -79,15 +77,17 @@ def derive_importpath(import_path, raising): return attr, obj - class Notset: def __repr__(self): return "" + notset = Notset() + class monkeypatch: """ Object keeping a record of setattr/item/env/syspath changes. """ + def __init__(self): self._setattr = [] self._setitem = [] @@ -114,14 +114,14 @@ class monkeypatch: if value is notset: if not isinstance(target, _basestring): raise TypeError("use setattr(target, name, value) or " - "setattr(target, value) with target being a dotted " - "import string") + "setattr(target, value) with target being a dotted " + "import string") value = name name, target = derive_importpath(target, raising) oldval = getattr(target, name, notset) if raising and oldval is notset: - raise AttributeError("%r has no attribute %r" %(target, name)) + raise AttributeError("%r has no attribute %r" % (target, name)) # avoid class descriptors like staticmethod/classmethod if inspect.isclass(target): @@ -233,7 +233,7 @@ class monkeypatch: try: del dictionary[name] except KeyError: - pass # was already deleted, so we have the desired state + pass # was already deleted, so we have the desired state else: dictionary[name] = value self._setitem[:] = [] diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 49db0bada..99437fe68 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -1,9 +1,11 @@ -import os, sys +import os +import sys import textwrap import pytest from _pytest.monkeypatch import monkeypatch as MonkeyPatch + def pytest_funcarg__mp(request): cwd = os.getcwd() sys_path = list(sys.path) @@ -15,9 +17,11 @@ def pytest_funcarg__mp(request): request.addfinalizer(cleanup) return MonkeyPatch() + def test_setattr(): class A: x = 1 + monkeypatch = MonkeyPatch() pytest.raises(AttributeError, "monkeypatch.setattr(A, 'notexists', 2)") monkeypatch.setattr(A, 'y', 2, raising=False) @@ -34,9 +38,10 @@ def test_setattr(): assert A.x == 1 A.x = 5 - monkeypatch.undo() # double-undo makes no modification + monkeypatch.undo() # double-undo makes no modification assert A.x == 5 + class TestSetattrWithImportPath: def test_string_expression(self, monkeypatch): monkeypatch.setattr("os.path.abspath", lambda x: "hello2") @@ -75,9 +80,11 @@ class TestSetattrWithImportPath: monkeypatch.undo() assert os.path.abspath + def test_delattr(): class A: x = 1 + monkeypatch = MonkeyPatch() monkeypatch.delattr(A, 'x') assert not hasattr(A, 'x') @@ -93,6 +100,7 @@ def test_delattr(): monkeypatch.undo() assert A.x == 1 + def test_setitem(): d = {'x': 1} monkeypatch = MonkeyPatch() @@ -110,6 +118,7 @@ def test_setitem(): monkeypatch.undo() assert d['x'] == 5 + def test_setitem_deleted_meanwhile(): d = {} monkeypatch = MonkeyPatch() @@ -118,6 +127,7 @@ def test_setitem_deleted_meanwhile(): monkeypatch.undo() assert not d + @pytest.mark.parametrize("before", [True, False]) def test_setenv_deleted_meanwhile(before): key = "qwpeoip123" @@ -133,6 +143,7 @@ def test_setenv_deleted_meanwhile(before): else: assert key not in os.environ + def test_delitem(): d = {'x': 1} monkeypatch = MonkeyPatch() @@ -149,6 +160,7 @@ def test_delitem(): monkeypatch.undo() assert d == {'hello': 'world', 'x': 1} + def test_setenv(): monkeypatch = MonkeyPatch() monkeypatch.setenv('XYZ123', 2) @@ -157,6 +169,7 @@ def test_setenv(): monkeypatch.undo() assert 'XYZ123' not in os.environ + def test_delenv(): name = 'xyz1234' assert name not in os.environ @@ -177,6 +190,7 @@ def test_delenv(): if name in os.environ: del os.environ[name] + def test_setenv_prepend(): import os monkeypatch = MonkeyPatch() @@ -187,6 +201,7 @@ def test_setenv_prepend(): monkeypatch.undo() assert 'XYZ123' not in os.environ + def test_monkeypatch_plugin(testdir): reprec = testdir.inline_runsource(""" def test_method(monkeypatch): @@ -195,6 +210,7 @@ def test_monkeypatch_plugin(testdir): res = reprec.countoutcomes() assert tuple(res) == (1, 0, 0), res + def test_syspath_prepend(mp): old = list(sys.path) mp.syspath_prepend('world') @@ -206,6 +222,7 @@ def test_syspath_prepend(mp): mp.undo() assert sys.path == old + def test_syspath_prepend_double_undo(mp): mp.syspath_prepend('hello world') mp.undo() @@ -213,20 +230,24 @@ def test_syspath_prepend_double_undo(mp): mp.undo() assert sys.path[-1] == 'more hello world' + def test_chdir_with_path_local(mp, tmpdir): mp.chdir(tmpdir) assert os.getcwd() == tmpdir.strpath + def test_chdir_with_str(mp, tmpdir): mp.chdir(tmpdir.strpath) assert os.getcwd() == tmpdir.strpath + def test_chdir_undo(mp, tmpdir): cwd = os.getcwd() mp.chdir(tmpdir) mp.undo() assert os.getcwd() == cwd + def test_chdir_double_undo(mp, tmpdir): mp.chdir(tmpdir.strpath) mp.undo() @@ -234,6 +255,7 @@ def test_chdir_double_undo(mp, tmpdir): mp.undo() assert os.getcwd() == tmpdir.strpath + def test_issue185_time_breaks(testdir): testdir.makepyfile(""" import time @@ -247,6 +269,7 @@ def test_issue185_time_breaks(testdir): *1 passed* """) + def test_importerror(testdir): p = testdir.mkpydir("package") p.join("a.py").write(textwrap.dedent("""\ @@ -275,11 +298,12 @@ class SampleNewInherit(SampleNew): class SampleOld: - #oldstyle on python2 + # oldstyle on python2 @staticmethod def hello(): return True + class SampleOldInherit(SampleOld): pass @@ -296,5 +320,3 @@ def test_issue156_undo_staticmethod(Sample): monkeypatch.undo() assert Sample.hello() - - From 60e9698530c7cda7d46cd8046ab26b48d22a2aa6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 23 Jan 2016 19:12:10 +0100 Subject: [PATCH 2/8] fix issue 1338 --- _pytest/monkeypatch.py | 77 ++++++++++++++++++------------------- testing/test_monkeypatch.py | 8 ++++ 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 97aa67c6c..74e29d94f 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -31,50 +31,47 @@ def pytest_funcarg__monkeypatch(request): return mpatch +def resolve(name): + # simplified from zope.dottedname + parts = name.split('.') + + used = parts.pop(0) + found = __import__(used) + for part in parts: + used += '.' + part + try: + found = getattr(found, part) + except AttributeError: + try: + __import__(used) + except ImportError as ex: + expected = ex.message.split()[-1] + if expected == used: + raise + else: + raise ImportError( + 'import error in %s: %s' % (used, ex) + ) + try: + found = getattr(found, part) + except AttributeError: + raise AttributeError( + '%r object at %s has no attribute %r' %( + type(found).__name__, used, part + ) + ) + return found + + def derive_importpath(import_path, raising): - import pytest if not isinstance(import_path, _basestring) or "." not in import_path: raise TypeError("must be absolute import path string, not %r" % (import_path,)) - rest = [] - target = import_path - target_parts = set(target.split(".")) - while target: - try: - obj = __import__(target, None, None, "__doc__") - except ImportError as ex: - if hasattr(ex, 'name'): - # Python >= 3.3 - failed_name = ex.name - else: - match = RE_IMPORT_ERROR_NAME.match(ex.args[0]) - assert match - failed_name = match.group(1) - - if "." not in target: - __tracebackhide__ = True - pytest.fail("could not import any sub part: %s" % - import_path) - elif failed_name != target \ - and not any(p == failed_name for p in target_parts): - # target is importable but causes ImportError itself - __tracebackhide__ = True - pytest.fail("import error in %s: %s" % (target, ex.args[0])) - target, name = target.rsplit(".", 1) - rest.append(name) - else: - assert rest - try: - while len(rest) > 1: - attr = rest.pop() - obj = getattr(obj, attr) - attr = rest[0] - if raising: - getattr(obj, attr) - except AttributeError: - __tracebackhide__ = True - pytest.fail("object %r has no attribute %r" % (obj, attr)) - return attr, obj + module, attr = import_path.rsplit('.', 1) + target = resolve(module) + if raising: + getattr(target, attr) + return attr, target class Notset: diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 99437fe68..7270309a9 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -320,3 +320,11 @@ def test_issue156_undo_staticmethod(Sample): monkeypatch.undo() assert Sample.hello() + +def test_issue1338_name_resolving(): + pytest.importorskip('requests') + monkeypatch = MonkeyPatch() + try: + monkeypatch.delattr('requests.sessions.Session.request') + finally: + monkeypatch.undo() \ No newline at end of file From b825af2e661a584b1dac1e6c4758d51cf9072657 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 23 Jan 2016 19:31:17 +0100 Subject: [PATCH 3/8] pass trough annotated exceptions --- _pytest/monkeypatch.py | 24 +++++++++++++++--------- testing/test_monkeypatch.py | 6 +++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 74e29d94f..475004539 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -2,6 +2,7 @@ import os, sys import re +import pytest from py.builtin import _basestring @@ -52,17 +53,22 @@ def resolve(name): raise ImportError( 'import error in %s: %s' % (used, ex) ) - try: - found = getattr(found, part) - except AttributeError: - raise AttributeError( - '%r object at %s has no attribute %r' %( - type(found).__name__, used, part - ) - ) + found = annotated_getattr(found, part, used) return found +def annotated_getattr(obj, name, ann): + try: + obj = getattr(obj, name) + except AttributeError: + raise AttributeError( + '%r object at %s has no attribute %r' % ( + type(obj).__name__, ann, name + ) + ) + return obj + + def derive_importpath(import_path, raising): if not isinstance(import_path, _basestring) or "." not in import_path: raise TypeError("must be absolute import path string, not %r" % @@ -70,7 +76,7 @@ def derive_importpath(import_path, raising): module, attr = import_path.rsplit('.', 1) target = resolve(module) if raising: - getattr(target, attr) + annotated_getattr(target, attr, ann=module) return attr, target diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 7270309a9..048c942c8 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -62,11 +62,11 @@ class TestSetattrWithImportPath: pytest.raises(TypeError, lambda: monkeypatch.setattr(None, None)) def test_unknown_import(self, monkeypatch): - pytest.raises(pytest.fail.Exception, + pytest.raises(ImportError, lambda: monkeypatch.setattr("unkn123.classx", None)) def test_unknown_attr(self, monkeypatch): - pytest.raises(pytest.fail.Exception, + pytest.raises(AttributeError, lambda: monkeypatch.setattr("os.path.qweqwe", None)) def test_unknown_attr_non_raising(self, monkeypatch): @@ -283,7 +283,7 @@ def test_importerror(testdir): """)) result = testdir.runpytest() result.stdout.fnmatch_lines(""" - *import error in package.a.x: No module named {0}doesnotexist{0}* + *import error in package.a: No module named {0}doesnotexist{0}* """.format("'" if sys.version_info > (3, 0) else "")) From d028fe1e662a5cf9e3561c606444f103a611a558 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 23 Jan 2016 20:29:54 +0100 Subject: [PATCH 4/8] remove unused import --- _pytest/monkeypatch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 475004539..43b8c2fe1 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -2,7 +2,6 @@ import os, sys import re -import pytest from py.builtin import _basestring From cd9e30b2215604bd601d00ecbb9989002aac9be5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Jan 2016 00:40:27 +0100 Subject: [PATCH 5/8] work around python 2/3 difference by using str(exception) --- _pytest/monkeypatch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 43b8c2fe1..4431d25b3 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -45,7 +45,8 @@ def resolve(name): try: __import__(used) except ImportError as ex: - expected = ex.message.split()[-1] + # str is used for py2 vs py3 + expected = str(ex).split()[-1] if expected == used: raise else: From cb6181255e5f88aeaecb0eab4d510747862d3738 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Jan 2016 00:45:59 +0100 Subject: [PATCH 6/8] changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index e0f64a11b..9c3064bc4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ 2.8.7.dev1 ---------- +- fix #1338: use predictable object resolution for monkeypatch' 2.8.6 ----- From 2d05f831fed55911bb3dd4a5cf905aeee8cdcc88 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Jan 2016 12:28:14 +0100 Subject: [PATCH 7/8] monkeypatch: unnest handling code this avoid python3 nested exceptions --- _pytest/monkeypatch.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 4431d25b3..d4c169d37 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -42,18 +42,23 @@ def resolve(name): try: found = getattr(found, part) except AttributeError: - try: - __import__(used) - except ImportError as ex: - # str is used for py2 vs py3 - expected = str(ex).split()[-1] - if expected == used: - raise - else: - raise ImportError( - 'import error in %s: %s' % (used, ex) - ) - found = annotated_getattr(found, part, used) + pass + else: + continue + # we use explicit un-nesting of the handling block in order + # to avoid nested exceptions on python 3 + try: + __import__(used) + except ImportError as ex: + # str is used for py2 vs py3 + expected = str(ex).split()[-1] + if expected == used: + raise + else: + raise ImportError( + 'import error in %s: %s' % (used, ex) + ) + found = annotated_getattr(found, part, used) return found From 56c5db6e12714cec27c1c480eb078c795d3a7389 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Jan 2016 12:30:38 +0100 Subject: [PATCH 8/8] add requests dependency to tox.ini to ensure all monkeypatch tests run --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 90468dbcf..64d84b2f3 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ passenv = USER USERNAME deps= nose mock + requests [testenv:py26] commands= py.test --lsof -rfsxX {posargs:testing}