Skip to content

Commit d53e449

Browse files
committed
Improve performance of assertion rewriting. Fixes #3918
1 parent 4345efa commit d53e449

File tree

5 files changed

+63
-17
lines changed

5 files changed

+63
-17
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ env/
3838
.ropeproject
3939
.idea
4040
.hypothesis
41+
.pydevproject
42+
.project
43+
.settings

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Endre Galaczi
7171
Eric Hunsberger
7272
Eric Siegerman
7373
Erik M. Bray
74+
Fabio Zadrozny
7475
Feng Ma
7576
Florian Bruhin
7677
Floris Bruynooghe

changelog/3918.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve performance of assertion rewriting.

src/_pytest/assertion/rewrite.py

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,20 @@ def __init__(self, config):
6767
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
6868
# which might result in infinite recursion (#3506)
6969
self._writing_pyc = False
70+
self._basenames_to_check_rewrite = set('conftest',)
71+
self._marked_for_rewrite_cache = {}
72+
self._session_paths_checked = False
7073

7174
def set_session(self, session):
7275
self.session = session
76+
self._session_paths_checked = False
7377

7478
def find_module(self, name, path=None):
7579
if self._writing_pyc:
7680
return None
7781
state = self.config._assertstate
82+
if self._early_rewrite_bailout(name, state):
83+
return None
7884
state.trace("find_module called for: %s" % name)
7985
names = name.rsplit(".", 1)
8086
lastname = names[-1]
@@ -166,6 +172,41 @@ def find_module(self, name, path=None):
166172
self.modules[name] = co, pyc
167173
return self
168174

175+
def _early_rewrite_bailout(self, name, state):
176+
"""
177+
This is a fast way to get out of rewriting modules. Profiling has
178+
shown that the call to imp.find_module (inside of the find_module
179+
from this class) is a major slowdown, so, this method tries to
180+
filter what we're sure won't be rewritten before getting to it.
181+
"""
182+
if not self._session_paths_checked and self.session is not None \
183+
and hasattr(self.session, '_initialpaths'):
184+
self._session_paths_checked = True
185+
for path in self.session._initialpaths:
186+
# Make something as c:/projects/my_project/path.py ->
187+
# ['c:', 'projects', 'my_project', 'path.py']
188+
parts = str(path).split(os.path.sep)
189+
# add 'path' to basenames to be checked.
190+
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
191+
192+
# Note: conftest already by default in _basenames_to_check_rewrite.
193+
parts = name.split('.')
194+
if parts[-1] in self._basenames_to_check_rewrite:
195+
return False
196+
197+
# For matching the name it must be as if it was a filename.
198+
parts[-1] = parts[-1] + '.py'
199+
fn_pypath = py.path.local(os.path.sep.join(parts))
200+
for pat in self.fnpats:
201+
if fn_pypath.fnmatch(pat):
202+
return False
203+
204+
if self._is_marked_for_rewrite(name, state):
205+
return False
206+
207+
state.trace("early skip of rewriting module: %s" % (name,))
208+
return True
209+
169210
def _should_rewrite(self, name, fn_pypath, state):
170211
# always rewrite conftest files
171212
fn = str(fn_pypath)
@@ -185,12 +226,20 @@ def _should_rewrite(self, name, fn_pypath, state):
185226
state.trace("matched test file %r" % (fn,))
186227
return True
187228

188-
for marked in self._must_rewrite:
189-
if name == marked or name.startswith(marked + "."):
190-
state.trace("matched marked file %r (from %r)" % (name, marked))
191-
return True
229+
return self._is_marked_for_rewrite(name, state)
192230

193-
return False
231+
def _is_marked_for_rewrite(self, name, state):
232+
try:
233+
return self._marked_for_rewrite_cache[name]
234+
except KeyError:
235+
for marked in self._must_rewrite:
236+
if name == marked or name.startswith(marked + "."):
237+
state.trace("matched marked file %r (from %r)" % (name, marked))
238+
self._marked_for_rewrite_cache[name] = True
239+
return True
240+
241+
self._marked_for_rewrite_cache[name] = False
242+
return False
194243

195244
def mark_rewrite(self, *names):
196245
"""Mark import names as needing to be rewritten.
@@ -207,6 +256,7 @@ def mark_rewrite(self, *names):
207256
):
208257
self._warn_already_imported(name)
209258
self._must_rewrite.update(names)
259+
self._marked_for_rewrite_cache.clear()
210260

211261
def _warn_already_imported(self, name):
212262
self.config.warn(
@@ -239,16 +289,6 @@ def load_module(self, name):
239289
raise
240290
return sys.modules[name]
241291

242-
def is_package(self, name):
243-
try:
244-
fd, fn, desc = imp.find_module(name)
245-
except ImportError:
246-
return False
247-
if fd is not None:
248-
fd.close()
249-
tp = desc[2]
250-
return tp == imp.PKG_DIRECTORY
251-
252292
@classmethod
253293
def _register_with_pkg_resources(cls):
254294
"""

src/_pytest/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,13 +441,14 @@ def _perform_collect(self, args, genitems):
441441
self.trace("perform_collect", self, args)
442442
self.trace.root.indent += 1
443443
self._notfound = []
444-
self._initialpaths = set()
444+
initialpaths = []
445445
self._initialparts = []
446446
self.items = items = []
447447
for arg in args:
448448
parts = self._parsearg(arg)
449449
self._initialparts.append(parts)
450-
self._initialpaths.add(parts[0])
450+
initialpaths.append(parts[0])
451+
self._initialpaths = frozenset(initialpaths)
451452
rep = collect_one_node(self)
452453
self.ihook.pytest_collectreport(report=rep)
453454
self.trace.root.indent -= 1

0 commit comments

Comments
 (0)