Switch from deprecated imp to importlib
This commit is contained in:
		
							parent
							
								
									3d01dd3adf
								
							
						
					
					
						commit
						4cd08f9b52
					
				|  | @ -0,0 +1 @@ | ||||||
|  | Switch from ``imp`` to ``importlib``. | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | Honor PEP 235 on case-insensitive file systems. | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | Test module is no longer double-imported when using ``--pyargs``. | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | Prevent "already imported" warnings from assertion rewriter when invoking pytest in-process multiple times. | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | Fix assertion rewriting in packages (``__init__.py``). | ||||||
|  | @ -1,18 +1,16 @@ | ||||||
| """Rewrite assertion AST to produce nice error messages""" | """Rewrite assertion AST to produce nice error messages""" | ||||||
| import ast | import ast | ||||||
| import errno | import errno | ||||||
| import imp | import importlib.machinery | ||||||
|  | import importlib.util | ||||||
| import itertools | import itertools | ||||||
| import marshal | import marshal | ||||||
| import os | import os | ||||||
| import re |  | ||||||
| import struct | import struct | ||||||
| import sys | import sys | ||||||
| import types | import types | ||||||
| from importlib.util import spec_from_file_location |  | ||||||
| 
 | 
 | ||||||
| import atomicwrites | import atomicwrites | ||||||
| import py |  | ||||||
| 
 | 
 | ||||||
| from _pytest._io.saferepr import saferepr | from _pytest._io.saferepr import saferepr | ||||||
| from _pytest.assertion import util | from _pytest.assertion import util | ||||||
|  | @ -23,23 +21,13 @@ from _pytest.pathlib import fnmatch_ex | ||||||
| from _pytest.pathlib import PurePath | from _pytest.pathlib import PurePath | ||||||
| 
 | 
 | ||||||
| # pytest caches rewritten pycs in __pycache__. | # pytest caches rewritten pycs in __pycache__. | ||||||
| if hasattr(imp, "get_tag"): | PYTEST_TAG = "{}-PYTEST".format(sys.implementation.cache_tag) | ||||||
|     PYTEST_TAG = imp.get_tag() + "-PYTEST" |  | ||||||
| else: |  | ||||||
|     if hasattr(sys, "pypy_version_info"): |  | ||||||
|         impl = "pypy" |  | ||||||
|     else: |  | ||||||
|         impl = "cpython" |  | ||||||
|     ver = sys.version_info |  | ||||||
|     PYTEST_TAG = "{}-{}{}-PYTEST".format(impl, ver[0], ver[1]) |  | ||||||
|     del ver, impl |  | ||||||
| 
 |  | ||||||
| PYC_EXT = ".py" + (__debug__ and "c" or "o") | PYC_EXT = ".py" + (__debug__ and "c" or "o") | ||||||
| PYC_TAIL = "." + PYTEST_TAG + PYC_EXT | PYC_TAIL = "." + PYTEST_TAG + PYC_EXT | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class AssertionRewritingHook: | class AssertionRewritingHook: | ||||||
|     """PEP302 Import hook which rewrites asserts.""" |     """PEP302/PEP451 import hook which rewrites asserts.""" | ||||||
| 
 | 
 | ||||||
|     def __init__(self, config): |     def __init__(self, config): | ||||||
|         self.config = config |         self.config = config | ||||||
|  | @ -48,7 +36,6 @@ class AssertionRewritingHook: | ||||||
|         except ValueError: |         except ValueError: | ||||||
|             self.fnpats = ["test_*.py", "*_test.py"] |             self.fnpats = ["test_*.py", "*_test.py"] | ||||||
|         self.session = None |         self.session = None | ||||||
|         self.modules = {} |  | ||||||
|         self._rewritten_names = set() |         self._rewritten_names = set() | ||||||
|         self._must_rewrite = set() |         self._must_rewrite = set() | ||||||
|         # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, |         # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, | ||||||
|  | @ -62,55 +49,51 @@ class AssertionRewritingHook: | ||||||
|         self.session = session |         self.session = session | ||||||
|         self._session_paths_checked = False |         self._session_paths_checked = False | ||||||
| 
 | 
 | ||||||
|     def _imp_find_module(self, name, path=None): |     # Indirection so we can mock calls to find_spec originated from the hook during testing | ||||||
|         """Indirection so we can mock calls to find_module originated from the hook during testing""" |     _find_spec = importlib.machinery.PathFinder.find_spec | ||||||
|         return imp.find_module(name, path) |  | ||||||
| 
 | 
 | ||||||
|     def find_module(self, name, path=None): |     def find_spec(self, name, path=None, target=None): | ||||||
|         if self._writing_pyc: |         if self._writing_pyc: | ||||||
|             return None |             return None | ||||||
|         state = self.config._assertstate |         state = self.config._assertstate | ||||||
|         if self._early_rewrite_bailout(name, state): |         if self._early_rewrite_bailout(name, state): | ||||||
|             return None |             return None | ||||||
|         state.trace("find_module called for: %s" % name) |         state.trace("find_module called for: %s" % name) | ||||||
|         names = name.rsplit(".", 1) |  | ||||||
|         lastname = names[-1] |  | ||||||
|         pth = None |  | ||||||
|         if path is not None: |  | ||||||
|             # Starting with Python 3.3, path is a _NamespacePath(), which |  | ||||||
|             # causes problems if not converted to list. |  | ||||||
|             path = list(path) |  | ||||||
|             if len(path) == 1: |  | ||||||
|                 pth = path[0] |  | ||||||
|         if pth is None: |  | ||||||
|             try: |  | ||||||
|                 fd, fn, desc = self._imp_find_module(lastname, path) |  | ||||||
|             except ImportError: |  | ||||||
|                 return None |  | ||||||
|             if fd is not None: |  | ||||||
|                 fd.close() |  | ||||||
|             tp = desc[2] |  | ||||||
|             if tp == imp.PY_COMPILED: |  | ||||||
|                 if hasattr(imp, "source_from_cache"): |  | ||||||
|                     try: |  | ||||||
|                         fn = imp.source_from_cache(fn) |  | ||||||
|                     except ValueError: |  | ||||||
|                         # Python 3 doesn't like orphaned but still-importable |  | ||||||
|                         # .pyc files. |  | ||||||
|                         fn = fn[:-1] |  | ||||||
|                 else: |  | ||||||
|                     fn = fn[:-1] |  | ||||||
|             elif tp != imp.PY_SOURCE: |  | ||||||
|                 # Don't know what this is. |  | ||||||
|                 return None |  | ||||||
|         else: |  | ||||||
|             fn = os.path.join(pth, name.rpartition(".")[2] + ".py") |  | ||||||
| 
 | 
 | ||||||
|         fn_pypath = py.path.local(fn) |         spec = self._find_spec(name, path) | ||||||
|         if not self._should_rewrite(name, fn_pypath, state): |         if ( | ||||||
|  |             # the import machinery could not find a file to import | ||||||
|  |             spec is None | ||||||
|  |             # this is a namespace package (without `__init__.py`) | ||||||
|  |             # there's nothing to rewrite there | ||||||
|  |             # python3.5 - python3.6: `namespace` | ||||||
|  |             # python3.7+: `None` | ||||||
|  |             or spec.origin in {None, "namespace"} | ||||||
|  |             # if the file doesn't exist, we can't rewrite it | ||||||
|  |             or not os.path.exists(spec.origin) | ||||||
|  |         ): | ||||||
|  |             return None | ||||||
|  |         else: | ||||||
|  |             fn = spec.origin | ||||||
|  | 
 | ||||||
|  |         if not self._should_rewrite(name, fn, state): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|         self._rewritten_names.add(name) |         return importlib.util.spec_from_file_location( | ||||||
|  |             name, | ||||||
|  |             fn, | ||||||
|  |             loader=self, | ||||||
|  |             submodule_search_locations=spec.submodule_search_locations, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def create_module(self, spec): | ||||||
|  |         return None  # default behaviour is fine | ||||||
|  | 
 | ||||||
|  |     def exec_module(self, module): | ||||||
|  |         fn = module.__spec__.origin | ||||||
|  |         state = self.config._assertstate | ||||||
|  | 
 | ||||||
|  |         self._rewritten_names.add(module.__name__) | ||||||
| 
 | 
 | ||||||
|         # The requested module looks like a test file, so rewrite it. This is |         # The requested module looks like a test file, so rewrite it. This is | ||||||
|         # the most magical part of the process: load the source, rewrite the |         # the most magical part of the process: load the source, rewrite the | ||||||
|  | @ -121,7 +104,7 @@ class AssertionRewritingHook: | ||||||
|         # cached pyc is always a complete, valid pyc. Operations on it must be |         # cached pyc is always a complete, valid pyc. Operations on it must be | ||||||
|         # atomic. POSIX's atomic rename comes in handy. |         # atomic. POSIX's atomic rename comes in handy. | ||||||
|         write = not sys.dont_write_bytecode |         write = not sys.dont_write_bytecode | ||||||
|         cache_dir = os.path.join(fn_pypath.dirname, "__pycache__") |         cache_dir = os.path.join(os.path.dirname(fn), "__pycache__") | ||||||
|         if write: |         if write: | ||||||
|             try: |             try: | ||||||
|                 os.mkdir(cache_dir) |                 os.mkdir(cache_dir) | ||||||
|  | @ -132,26 +115,23 @@ class AssertionRewritingHook: | ||||||
|                     # common case) or it's blocked by a non-dir node. In the |                     # common case) or it's blocked by a non-dir node. In the | ||||||
|                     # latter case, we'll ignore it in _write_pyc. |                     # latter case, we'll ignore it in _write_pyc. | ||||||
|                     pass |                     pass | ||||||
|                 elif e in [errno.ENOENT, errno.ENOTDIR]: |                 elif e in {errno.ENOENT, errno.ENOTDIR}: | ||||||
|                     # One of the path components was not a directory, likely |                     # One of the path components was not a directory, likely | ||||||
|                     # because we're in a zip file. |                     # because we're in a zip file. | ||||||
|                     write = False |                     write = False | ||||||
|                 elif e in [errno.EACCES, errno.EROFS, errno.EPERM]: |                 elif e in {errno.EACCES, errno.EROFS, errno.EPERM}: | ||||||
|                     state.trace("read only directory: %r" % fn_pypath.dirname) |                     state.trace("read only directory: %r" % os.path.dirname(fn)) | ||||||
|                     write = False |                     write = False | ||||||
|                 else: |                 else: | ||||||
|                     raise |                     raise | ||||||
|         cache_name = fn_pypath.basename[:-3] + PYC_TAIL |         cache_name = os.path.basename(fn)[:-3] + PYC_TAIL | ||||||
|         pyc = os.path.join(cache_dir, cache_name) |         pyc = os.path.join(cache_dir, cache_name) | ||||||
|         # Notice that even if we're in a read-only directory, I'm going |         # Notice that even if we're in a read-only directory, I'm going | ||||||
|         # to check for a cached pyc. This may not be optimal... |         # to check for a cached pyc. This may not be optimal... | ||||||
|         co = _read_pyc(fn_pypath, pyc, state.trace) |         co = _read_pyc(fn, pyc, state.trace) | ||||||
|         if co is None: |         if co is None: | ||||||
|             state.trace("rewriting {!r}".format(fn)) |             state.trace("rewriting {!r}".format(fn)) | ||||||
|             source_stat, co = _rewrite_test(self.config, fn_pypath) |             source_stat, co = _rewrite_test(fn) | ||||||
|             if co is None: |  | ||||||
|                 # Probably a SyntaxError in the test. |  | ||||||
|                 return None |  | ||||||
|             if write: |             if write: | ||||||
|                 self._writing_pyc = True |                 self._writing_pyc = True | ||||||
|                 try: |                 try: | ||||||
|  | @ -160,13 +140,11 @@ class AssertionRewritingHook: | ||||||
|                     self._writing_pyc = False |                     self._writing_pyc = False | ||||||
|         else: |         else: | ||||||
|             state.trace("found cached rewritten pyc for {!r}".format(fn)) |             state.trace("found cached rewritten pyc for {!r}".format(fn)) | ||||||
|         self.modules[name] = co, pyc |         exec(co, module.__dict__) | ||||||
|         return self |  | ||||||
| 
 | 
 | ||||||
|     def _early_rewrite_bailout(self, name, state): |     def _early_rewrite_bailout(self, name, state): | ||||||
|         """ |         """This is a fast way to get out of rewriting modules. Profiling has | ||||||
|         This is a fast way to get out of rewriting modules. Profiling has |         shown that the call to PathFinder.find_spec (inside of the find_spec | ||||||
|         shown that the call to imp.find_module (inside of the find_module |  | ||||||
|         from this class) is a major slowdown, so, this method tries to |         from this class) is a major slowdown, so, this method tries to | ||||||
|         filter what we're sure won't be rewritten before getting to it. |         filter what we're sure won't be rewritten before getting to it. | ||||||
|         """ |         """ | ||||||
|  | @ -201,10 +179,9 @@ class AssertionRewritingHook: | ||||||
|         state.trace("early skip of rewriting module: {}".format(name)) |         state.trace("early skip of rewriting module: {}".format(name)) | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|     def _should_rewrite(self, name, fn_pypath, state): |     def _should_rewrite(self, name, fn, state): | ||||||
|         # always rewrite conftest files |         # always rewrite conftest files | ||||||
|         fn = str(fn_pypath) |         if os.path.basename(fn) == "conftest.py": | ||||||
|         if fn_pypath.basename == "conftest.py": |  | ||||||
|             state.trace("rewriting conftest file: {!r}".format(fn)) |             state.trace("rewriting conftest file: {!r}".format(fn)) | ||||||
|             return True |             return True | ||||||
| 
 | 
 | ||||||
|  | @ -217,8 +194,9 @@ class AssertionRewritingHook: | ||||||
| 
 | 
 | ||||||
|         # modules not passed explicitly on the command line are only |         # modules not passed explicitly on the command line are only | ||||||
|         # rewritten if they match the naming convention for test files |         # rewritten if they match the naming convention for test files | ||||||
|  |         fn_path = PurePath(fn) | ||||||
|         for pat in self.fnpats: |         for pat in self.fnpats: | ||||||
|             if fn_pypath.fnmatch(pat): |             if fnmatch_ex(pat, fn_path): | ||||||
|                 state.trace("matched test file {!r}".format(fn)) |                 state.trace("matched test file {!r}".format(fn)) | ||||||
|                 return True |                 return True | ||||||
| 
 | 
 | ||||||
|  | @ -249,9 +227,10 @@ class AssertionRewritingHook: | ||||||
|             set(names).intersection(sys.modules).difference(self._rewritten_names) |             set(names).intersection(sys.modules).difference(self._rewritten_names) | ||||||
|         ) |         ) | ||||||
|         for name in already_imported: |         for name in already_imported: | ||||||
|  |             mod = sys.modules[name] | ||||||
|             if not AssertionRewriter.is_rewrite_disabled( |             if not AssertionRewriter.is_rewrite_disabled( | ||||||
|                 sys.modules[name].__doc__ or "" |                 mod.__doc__ or "" | ||||||
|             ): |             ) and not isinstance(mod.__loader__, type(self)): | ||||||
|                 self._warn_already_imported(name) |                 self._warn_already_imported(name) | ||||||
|         self._must_rewrite.update(names) |         self._must_rewrite.update(names) | ||||||
|         self._marked_for_rewrite_cache.clear() |         self._marked_for_rewrite_cache.clear() | ||||||
|  | @ -268,45 +247,8 @@ class AssertionRewritingHook: | ||||||
|             stacklevel=5, |             stacklevel=5, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     def load_module(self, name): |  | ||||||
|         co, pyc = self.modules.pop(name) |  | ||||||
|         if name in sys.modules: |  | ||||||
|             # If there is an existing module object named 'fullname' in |  | ||||||
|             # sys.modules, the loader must use that existing module. (Otherwise, |  | ||||||
|             # the reload() builtin will not work correctly.) |  | ||||||
|             mod = sys.modules[name] |  | ||||||
|         else: |  | ||||||
|             # I wish I could just call imp.load_compiled here, but __file__ has to |  | ||||||
|             # be set properly. In Python 3.2+, this all would be handled correctly |  | ||||||
|             # by load_compiled. |  | ||||||
|             mod = sys.modules[name] = imp.new_module(name) |  | ||||||
|         try: |  | ||||||
|             mod.__file__ = co.co_filename |  | ||||||
|             # Normally, this attribute is 3.2+. |  | ||||||
|             mod.__cached__ = pyc |  | ||||||
|             mod.__loader__ = self |  | ||||||
|             # Normally, this attribute is 3.4+ |  | ||||||
|             mod.__spec__ = spec_from_file_location(name, co.co_filename, loader=self) |  | ||||||
|             exec(co, mod.__dict__) |  | ||||||
|         except:  # noqa |  | ||||||
|             if name in sys.modules: |  | ||||||
|                 del sys.modules[name] |  | ||||||
|             raise |  | ||||||
|         return sys.modules[name] |  | ||||||
| 
 |  | ||||||
|     def is_package(self, name): |  | ||||||
|         try: |  | ||||||
|             fd, fn, desc = self._imp_find_module(name) |  | ||||||
|         except ImportError: |  | ||||||
|             return False |  | ||||||
|         if fd is not None: |  | ||||||
|             fd.close() |  | ||||||
|         tp = desc[2] |  | ||||||
|         return tp == imp.PKG_DIRECTORY |  | ||||||
| 
 |  | ||||||
|     def get_data(self, pathname): |     def get_data(self, pathname): | ||||||
|         """Optional PEP302 get_data API. |         """Optional PEP302 get_data API.""" | ||||||
|         """ |  | ||||||
|         with open(pathname, "rb") as f: |         with open(pathname, "rb") as f: | ||||||
|             return f.read() |             return f.read() | ||||||
| 
 | 
 | ||||||
|  | @ -314,15 +256,13 @@ class AssertionRewritingHook: | ||||||
| def _write_pyc(state, co, source_stat, pyc): | def _write_pyc(state, co, source_stat, pyc): | ||||||
|     # Technically, we don't have to have the same pyc format as |     # Technically, we don't have to have the same pyc format as | ||||||
|     # (C)Python, since these "pycs" should never be seen by builtin |     # (C)Python, since these "pycs" should never be seen by builtin | ||||||
|     # import. However, there's little reason deviate, and I hope |     # import. However, there's little reason deviate. | ||||||
|     # sometime to be able to use imp.load_compiled to load them. (See |  | ||||||
|     # the comment in load_module above.) |  | ||||||
|     try: |     try: | ||||||
|         with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp: |         with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp: | ||||||
|             fp.write(imp.get_magic()) |             fp.write(importlib.util.MAGIC_NUMBER) | ||||||
|             # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) |             # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) | ||||||
|             mtime = int(source_stat.mtime) & 0xFFFFFFFF |             mtime = int(source_stat.st_mtime) & 0xFFFFFFFF | ||||||
|             size = source_stat.size & 0xFFFFFFFF |             size = source_stat.st_size & 0xFFFFFFFF | ||||||
|             # "<LL" stands for 2 unsigned longs, little-ending |             # "<LL" stands for 2 unsigned longs, little-ending | ||||||
|             fp.write(struct.pack("<LL", mtime, size)) |             fp.write(struct.pack("<LL", mtime, size)) | ||||||
|             fp.write(marshal.dumps(co)) |             fp.write(marshal.dumps(co)) | ||||||
|  | @ -335,35 +275,14 @@ def _write_pyc(state, co, source_stat, pyc): | ||||||
|     return True |     return True | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| RN = b"\r\n" | def _rewrite_test(fn): | ||||||
| N = b"\n" |     """read and rewrite *fn* and return the code object.""" | ||||||
| 
 |     stat = os.stat(fn) | ||||||
| cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+") |     with open(fn, "rb") as f: | ||||||
| BOM_UTF8 = "\xef\xbb\xbf" |         source = f.read() | ||||||
| 
 |     tree = ast.parse(source, filename=fn) | ||||||
| 
 |     rewrite_asserts(tree, fn) | ||||||
| def _rewrite_test(config, fn): |     co = compile(tree, fn, "exec", dont_inherit=True) | ||||||
|     """Try to read and rewrite *fn* and return the code object.""" |  | ||||||
|     state = config._assertstate |  | ||||||
|     try: |  | ||||||
|         stat = fn.stat() |  | ||||||
|         source = fn.read("rb") |  | ||||||
|     except EnvironmentError: |  | ||||||
|         return None, None |  | ||||||
|     try: |  | ||||||
|         tree = ast.parse(source, filename=fn.strpath) |  | ||||||
|     except SyntaxError: |  | ||||||
|         # Let this pop up again in the real import. |  | ||||||
|         state.trace("failed to parse: {!r}".format(fn)) |  | ||||||
|         return None, None |  | ||||||
|     rewrite_asserts(tree, fn, config) |  | ||||||
|     try: |  | ||||||
|         co = compile(tree, fn.strpath, "exec", dont_inherit=True) |  | ||||||
|     except SyntaxError: |  | ||||||
|         # It's possible that this error is from some bug in the |  | ||||||
|         # assertion rewriting, but I don't know of a fast way to tell. |  | ||||||
|         state.trace("failed to compile: {!r}".format(fn)) |  | ||||||
|         return None, None |  | ||||||
|     return stat, co |     return stat, co | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -378,8 +297,9 @@ def _read_pyc(source, pyc, trace=lambda x: None): | ||||||
|         return None |         return None | ||||||
|     with fp: |     with fp: | ||||||
|         try: |         try: | ||||||
|             mtime = int(source.mtime()) |             stat_result = os.stat(source) | ||||||
|             size = source.size() |             mtime = int(stat_result.st_mtime) | ||||||
|  |             size = stat_result.st_size | ||||||
|             data = fp.read(12) |             data = fp.read(12) | ||||||
|         except EnvironmentError as e: |         except EnvironmentError as e: | ||||||
|             trace("_read_pyc({}): EnvironmentError {}".format(source, e)) |             trace("_read_pyc({}): EnvironmentError {}".format(source, e)) | ||||||
|  | @ -387,7 +307,7 @@ def _read_pyc(source, pyc, trace=lambda x: None): | ||||||
|         # Check for invalid or out of date pyc file. |         # Check for invalid or out of date pyc file. | ||||||
|         if ( |         if ( | ||||||
|             len(data) != 12 |             len(data) != 12 | ||||||
|             or data[:4] != imp.get_magic() |             or data[:4] != importlib.util.MAGIC_NUMBER | ||||||
|             or struct.unpack("<LL", data[4:]) != (mtime & 0xFFFFFFFF, size & 0xFFFFFFFF) |             or struct.unpack("<LL", data[4:]) != (mtime & 0xFFFFFFFF, size & 0xFFFFFFFF) | ||||||
|         ): |         ): | ||||||
|             trace("_read_pyc(%s): invalid or out of date pyc" % source) |             trace("_read_pyc(%s): invalid or out of date pyc" % source) | ||||||
|  | @ -403,9 +323,9 @@ def _read_pyc(source, pyc, trace=lambda x: None): | ||||||
|         return co |         return co | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def rewrite_asserts(mod, module_path=None, config=None): | def rewrite_asserts(mod, module_path=None): | ||||||
|     """Rewrite the assert statements in mod.""" |     """Rewrite the assert statements in mod.""" | ||||||
|     AssertionRewriter(module_path, config).run(mod) |     AssertionRewriter(module_path).run(mod) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _saferepr(obj): | def _saferepr(obj): | ||||||
|  | @ -586,10 +506,9 @@ class AssertionRewriter(ast.NodeVisitor): | ||||||
| 
 | 
 | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     def __init__(self, module_path, config): |     def __init__(self, module_path): | ||||||
|         super().__init__() |         super().__init__() | ||||||
|         self.module_path = module_path |         self.module_path = module_path | ||||||
|         self.config = config |  | ||||||
| 
 | 
 | ||||||
|     def run(self, mod): |     def run(self, mod): | ||||||
|         """Find all assert statements in *mod* and rewrite them.""" |         """Find all assert statements in *mod* and rewrite them.""" | ||||||
|  | @ -758,7 +677,7 @@ class AssertionRewriter(ast.NodeVisitor): | ||||||
|                     "assertion is always true, perhaps remove parentheses?" |                     "assertion is always true, perhaps remove parentheses?" | ||||||
|                 ), |                 ), | ||||||
|                 category=None, |                 category=None, | ||||||
|                 filename=str(self.module_path), |                 filename=self.module_path, | ||||||
|                 lineno=assert_.lineno, |                 lineno=assert_.lineno, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -817,7 +736,7 @@ class AssertionRewriter(ast.NodeVisitor): | ||||||
|         AST_NONE = ast.parse("None").body[0].value |         AST_NONE = ast.parse("None").body[0].value | ||||||
|         val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE]) |         val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE]) | ||||||
|         send_warning = ast.parse( |         send_warning = ast.parse( | ||||||
|             """ |             """\ | ||||||
| from _pytest.warning_types import PytestAssertRewriteWarning | from _pytest.warning_types import PytestAssertRewriteWarning | ||||||
| from warnings import warn_explicit | from warnings import warn_explicit | ||||||
| warn_explicit( | warn_explicit( | ||||||
|  | @ -827,7 +746,7 @@ warn_explicit( | ||||||
|     lineno={lineno}, |     lineno={lineno}, | ||||||
| ) | ) | ||||||
|             """.format( |             """.format( | ||||||
|                 filename=module_path.strpath, lineno=lineno |                 filename=module_path, lineno=lineno | ||||||
|             ) |             ) | ||||||
|         ).body |         ).body | ||||||
|         return ast.If(val_is_none, send_warning, []) |         return ast.If(val_is_none, send_warning, []) | ||||||
|  |  | ||||||
|  | @ -2,8 +2,8 @@ | ||||||
| import enum | import enum | ||||||
| import fnmatch | import fnmatch | ||||||
| import functools | import functools | ||||||
|  | import importlib | ||||||
| import os | import os | ||||||
| import pkgutil |  | ||||||
| import sys | import sys | ||||||
| import warnings | import warnings | ||||||
| 
 | 
 | ||||||
|  | @ -630,21 +630,15 @@ class Session(nodes.FSCollector): | ||||||
|     def _tryconvertpyarg(self, x): |     def _tryconvertpyarg(self, x): | ||||||
|         """Convert a dotted module name to path.""" |         """Convert a dotted module name to path.""" | ||||||
|         try: |         try: | ||||||
|             loader = pkgutil.find_loader(x) |             spec = importlib.util.find_spec(x) | ||||||
|         except ImportError: |         except (ValueError, ImportError): | ||||||
|             return x |             return x | ||||||
|         if loader is None: |         if spec is None or spec.origin in {None, "namespace"}: | ||||||
|             return x |             return x | ||||||
|         # This method is sometimes invoked when AssertionRewritingHook, which |         elif spec.submodule_search_locations: | ||||||
|         # does not define a get_filename method, is already in place: |             return os.path.dirname(spec.origin) | ||||||
|         try: |         else: | ||||||
|             path = loader.get_filename(x) |             return spec.origin | ||||||
|         except AttributeError: |  | ||||||
|             # Retrieve path from AssertionRewritingHook: |  | ||||||
|             path = loader.modules[x][0].co_filename |  | ||||||
|         if loader.is_package(x): |  | ||||||
|             path = os.path.dirname(path) |  | ||||||
|         return path |  | ||||||
| 
 | 
 | ||||||
|     def _parsearg(self, arg): |     def _parsearg(self, arg): | ||||||
|         """ return (fspath, names) tuple after checking the file exists. """ |         """ return (fspath, names) tuple after checking the file exists. """ | ||||||
|  |  | ||||||
|  | @ -294,6 +294,8 @@ def fnmatch_ex(pattern, path): | ||||||
|         name = path.name |         name = path.name | ||||||
|     else: |     else: | ||||||
|         name = str(path) |         name = str(path) | ||||||
|  |         if path.is_absolute() and not os.path.isabs(pattern): | ||||||
|  |             pattern = "*{}{}".format(os.sep, pattern) | ||||||
|     return fnmatch.fnmatch(name, pattern) |     return fnmatch.fnmatch(name, pattern) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| """(disabled by default) support for testing pytest and pytest plugins.""" | """(disabled by default) support for testing pytest and pytest plugins.""" | ||||||
| import gc | import gc | ||||||
|  | import importlib | ||||||
| import os | import os | ||||||
| import platform | import platform | ||||||
| import re | import re | ||||||
|  | @ -16,7 +17,6 @@ import py | ||||||
| import pytest | import pytest | ||||||
| from _pytest._code import Source | from _pytest._code import Source | ||||||
| from _pytest._io.saferepr import saferepr | from _pytest._io.saferepr import saferepr | ||||||
| from _pytest.assertion.rewrite import AssertionRewritingHook |  | ||||||
| from _pytest.capture import MultiCapture | from _pytest.capture import MultiCapture | ||||||
| from _pytest.capture import SysCapture | from _pytest.capture import SysCapture | ||||||
| from _pytest.main import ExitCode | from _pytest.main import ExitCode | ||||||
|  | @ -787,6 +787,11 @@ class Testdir: | ||||||
| 
 | 
 | ||||||
|         :return: a :py:class:`HookRecorder` instance |         :return: a :py:class:`HookRecorder` instance | ||||||
|         """ |         """ | ||||||
|  |         # (maybe a cpython bug?) the importlib cache sometimes isn't updated | ||||||
|  |         # properly between file creation and inline_run (especially if imports | ||||||
|  |         # are interspersed with file creation) | ||||||
|  |         importlib.invalidate_caches() | ||||||
|  | 
 | ||||||
|         plugins = list(plugins) |         plugins = list(plugins) | ||||||
|         finalizers = [] |         finalizers = [] | ||||||
|         try: |         try: | ||||||
|  | @ -796,18 +801,6 @@ class Testdir: | ||||||
|                 mp_run.setenv(k, v) |                 mp_run.setenv(k, v) | ||||||
|             finalizers.append(mp_run.undo) |             finalizers.append(mp_run.undo) | ||||||
| 
 | 
 | ||||||
|             # When running pytest inline any plugins active in the main test |  | ||||||
|             # process are already imported.  So this disables the warning which |  | ||||||
|             # will trigger to say they can no longer be rewritten, which is |  | ||||||
|             # fine as they have already been rewritten. |  | ||||||
|             orig_warn = AssertionRewritingHook._warn_already_imported |  | ||||||
| 
 |  | ||||||
|             def revert_warn_already_imported(): |  | ||||||
|                 AssertionRewritingHook._warn_already_imported = orig_warn |  | ||||||
| 
 |  | ||||||
|             finalizers.append(revert_warn_already_imported) |  | ||||||
|             AssertionRewritingHook._warn_already_imported = lambda *a: None |  | ||||||
| 
 |  | ||||||
|             # Any sys.module or sys.path changes done while running pytest |             # Any sys.module or sys.path changes done while running pytest | ||||||
|             # inline should be reverted after the test run completes to avoid |             # inline should be reverted after the test run completes to avoid | ||||||
|             # clashing with later inline tests run within the same pytest test, |             # clashing with later inline tests run within the same pytest test, | ||||||
|  |  | ||||||
|  | @ -633,6 +633,19 @@ class TestInvocationVariants: | ||||||
| 
 | 
 | ||||||
|         result.stdout.fnmatch_lines(["collected*0*items*/*1*errors"]) |         result.stdout.fnmatch_lines(["collected*0*items*/*1*errors"]) | ||||||
| 
 | 
 | ||||||
|  |     def test_pyargs_only_imported_once(self, testdir): | ||||||
|  |         pkg = testdir.mkpydir("foo") | ||||||
|  |         pkg.join("test_foo.py").write("print('hello from test_foo')\ndef test(): pass") | ||||||
|  |         pkg.join("conftest.py").write( | ||||||
|  |             "def pytest_configure(config): print('configuring')" | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         result = testdir.runpytest("--pyargs", "foo.test_foo", "-s", syspathinsert=True) | ||||||
|  |         # should only import once | ||||||
|  |         assert result.outlines.count("hello from test_foo") == 1 | ||||||
|  |         # should only configure once | ||||||
|  |         assert result.outlines.count("configuring") == 1 | ||||||
|  | 
 | ||||||
|     def test_cmdline_python_package(self, testdir, monkeypatch): |     def test_cmdline_python_package(self, testdir, monkeypatch): | ||||||
|         import warnings |         import warnings | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -137,8 +137,8 @@ class TestImportHookInstallation: | ||||||
|             "hamster.py": "", |             "hamster.py": "", | ||||||
|             "test_foo.py": """\ |             "test_foo.py": """\ | ||||||
|                 def test_foo(pytestconfig): |                 def test_foo(pytestconfig): | ||||||
|                     assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None |                     assert pytestconfig.pluginmanager.rewrite_hook.find_spec('ham') is not None | ||||||
|                     assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None |                     assert pytestconfig.pluginmanager.rewrite_hook.find_spec('hamster') is None | ||||||
|             """, |             """, | ||||||
|         } |         } | ||||||
|         testdir.makepyfile(**contents) |         testdir.makepyfile(**contents) | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import ast | import ast | ||||||
| import glob | import glob | ||||||
|  | import importlib | ||||||
| import os | import os | ||||||
| import py_compile | import py_compile | ||||||
| import stat | import stat | ||||||
|  | @ -117,6 +118,37 @@ class TestAssertionRewrite: | ||||||
|         result = testdir.runpytest_subprocess() |         result = testdir.runpytest_subprocess() | ||||||
|         assert "warnings" not in "".join(result.outlines) |         assert "warnings" not in "".join(result.outlines) | ||||||
| 
 | 
 | ||||||
|  |     def test_rewrites_plugin_as_a_package(self, testdir): | ||||||
|  |         pkgdir = testdir.mkpydir("plugin") | ||||||
|  |         pkgdir.join("__init__.py").write( | ||||||
|  |             "import pytest\n" | ||||||
|  |             "@pytest.fixture\n" | ||||||
|  |             "def special_asserter():\n" | ||||||
|  |             "    def special_assert(x, y):\n" | ||||||
|  |             "        assert x == y\n" | ||||||
|  |             "    return special_assert\n" | ||||||
|  |         ) | ||||||
|  |         testdir.makeconftest('pytest_plugins = ["plugin"]') | ||||||
|  |         testdir.makepyfile("def test(special_asserter): special_asserter(1, 2)\n") | ||||||
|  |         result = testdir.runpytest() | ||||||
|  |         result.stdout.fnmatch_lines(["*assert 1 == 2*"]) | ||||||
|  | 
 | ||||||
|  |     def test_honors_pep_235(self, testdir, monkeypatch): | ||||||
|  |         # note: couldn't make it fail on macos with a single `sys.path` entry | ||||||
|  |         # note: these modules are named `test_*` to trigger rewriting | ||||||
|  |         testdir.tmpdir.join("test_y.py").write("x = 1") | ||||||
|  |         xdir = testdir.tmpdir.join("x").ensure_dir() | ||||||
|  |         xdir.join("test_Y").ensure_dir().join("__init__.py").write("x = 2") | ||||||
|  |         testdir.makepyfile( | ||||||
|  |             "import test_y\n" | ||||||
|  |             "import test_Y\n" | ||||||
|  |             "def test():\n" | ||||||
|  |             "    assert test_y.x == 1\n" | ||||||
|  |             "    assert test_Y.x == 2\n" | ||||||
|  |         ) | ||||||
|  |         monkeypatch.syspath_prepend(xdir) | ||||||
|  |         testdir.runpytest().assert_outcomes(passed=1) | ||||||
|  | 
 | ||||||
|     def test_name(self, request): |     def test_name(self, request): | ||||||
|         def f(): |         def f(): | ||||||
|             assert False |             assert False | ||||||
|  | @ -831,8 +863,9 @@ def test_rewritten(): | ||||||
|         monkeypatch.setattr( |         monkeypatch.setattr( | ||||||
|             hook, "_warn_already_imported", lambda code, msg: warnings.append(msg) |             hook, "_warn_already_imported", lambda code, msg: warnings.append(msg) | ||||||
|         ) |         ) | ||||||
|         hook.find_module("test_remember_rewritten_modules") |         spec = hook.find_spec("test_remember_rewritten_modules") | ||||||
|         hook.load_module("test_remember_rewritten_modules") |         module = importlib.util.module_from_spec(spec) | ||||||
|  |         hook.exec_module(module) | ||||||
|         hook.mark_rewrite("test_remember_rewritten_modules") |         hook.mark_rewrite("test_remember_rewritten_modules") | ||||||
|         hook.mark_rewrite("test_remember_rewritten_modules") |         hook.mark_rewrite("test_remember_rewritten_modules") | ||||||
|         assert warnings == [] |         assert warnings == [] | ||||||
|  | @ -872,33 +905,6 @@ def test_rewritten(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestAssertionRewriteHookDetails: | class TestAssertionRewriteHookDetails: | ||||||
|     def test_loader_is_package_false_for_module(self, testdir): |  | ||||||
|         testdir.makepyfile( |  | ||||||
|             test_fun=""" |  | ||||||
|             def test_loader(): |  | ||||||
|                 assert not __loader__.is_package(__name__) |  | ||||||
|             """ |  | ||||||
|         ) |  | ||||||
|         result = testdir.runpytest() |  | ||||||
|         result.stdout.fnmatch_lines(["* 1 passed*"]) |  | ||||||
| 
 |  | ||||||
|     def test_loader_is_package_true_for_package(self, testdir): |  | ||||||
|         testdir.makepyfile( |  | ||||||
|             test_fun=""" |  | ||||||
|             def test_loader(): |  | ||||||
|                 assert not __loader__.is_package(__name__) |  | ||||||
| 
 |  | ||||||
|             def test_fun(): |  | ||||||
|                 assert __loader__.is_package('fun') |  | ||||||
| 
 |  | ||||||
|             def test_missing(): |  | ||||||
|                 assert not __loader__.is_package('pytest_not_there') |  | ||||||
|             """ |  | ||||||
|         ) |  | ||||||
|         testdir.mkpydir("fun") |  | ||||||
|         result = testdir.runpytest() |  | ||||||
|         result.stdout.fnmatch_lines(["* 3 passed*"]) |  | ||||||
| 
 |  | ||||||
|     def test_sys_meta_path_munged(self, testdir): |     def test_sys_meta_path_munged(self, testdir): | ||||||
|         testdir.makepyfile( |         testdir.makepyfile( | ||||||
|             """ |             """ | ||||||
|  | @ -917,7 +923,7 @@ class TestAssertionRewriteHookDetails: | ||||||
|         state = AssertionState(config, "rewrite") |         state = AssertionState(config, "rewrite") | ||||||
|         source_path = tmpdir.ensure("source.py") |         source_path = tmpdir.ensure("source.py") | ||||||
|         pycpath = tmpdir.join("pyc").strpath |         pycpath = tmpdir.join("pyc").strpath | ||||||
|         assert _write_pyc(state, [1], source_path.stat(), pycpath) |         assert _write_pyc(state, [1], os.stat(source_path.strpath), pycpath) | ||||||
| 
 | 
 | ||||||
|         @contextmanager |         @contextmanager | ||||||
|         def atomic_write_failed(fn, mode="r", overwrite=False): |         def atomic_write_failed(fn, mode="r", overwrite=False): | ||||||
|  | @ -979,7 +985,7 @@ class TestAssertionRewriteHookDetails: | ||||||
|         assert len(contents) > strip_bytes |         assert len(contents) > strip_bytes | ||||||
|         pyc.write(contents[:strip_bytes], mode="wb") |         pyc.write(contents[:strip_bytes], mode="wb") | ||||||
| 
 | 
 | ||||||
|         assert _read_pyc(source, str(pyc)) is None  # no error |         assert _read_pyc(str(source), str(pyc)) is None  # no error | ||||||
| 
 | 
 | ||||||
|     def test_reload_is_same(self, testdir): |     def test_reload_is_same(self, testdir): | ||||||
|         # A file that will be picked up during collecting. |         # A file that will be picked up during collecting. | ||||||
|  | @ -1186,14 +1192,17 @@ def test_rewrite_infinite_recursion(testdir, pytestconfig, monkeypatch): | ||||||
|         # make a note that we have called _write_pyc |         # make a note that we have called _write_pyc | ||||||
|         write_pyc_called.append(True) |         write_pyc_called.append(True) | ||||||
|         # try to import a module at this point: we should not try to rewrite this module |         # try to import a module at this point: we should not try to rewrite this module | ||||||
|         assert hook.find_module("test_bar") is None |         assert hook.find_spec("test_bar") is None | ||||||
|         return original_write_pyc(*args, **kwargs) |         return original_write_pyc(*args, **kwargs) | ||||||
| 
 | 
 | ||||||
|     monkeypatch.setattr(rewrite, "_write_pyc", spy_write_pyc) |     monkeypatch.setattr(rewrite, "_write_pyc", spy_write_pyc) | ||||||
|     monkeypatch.setattr(sys, "dont_write_bytecode", False) |     monkeypatch.setattr(sys, "dont_write_bytecode", False) | ||||||
| 
 | 
 | ||||||
|     hook = AssertionRewritingHook(pytestconfig) |     hook = AssertionRewritingHook(pytestconfig) | ||||||
|     assert hook.find_module("test_foo") is not None |     spec = hook.find_spec("test_foo") | ||||||
|  |     assert spec is not None | ||||||
|  |     module = importlib.util.module_from_spec(spec) | ||||||
|  |     hook.exec_module(module) | ||||||
|     assert len(write_pyc_called) == 1 |     assert len(write_pyc_called) == 1 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1201,11 +1210,11 @@ class TestEarlyRewriteBailout: | ||||||
|     @pytest.fixture |     @pytest.fixture | ||||||
|     def hook(self, pytestconfig, monkeypatch, testdir): |     def hook(self, pytestconfig, monkeypatch, testdir): | ||||||
|         """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track |         """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track | ||||||
|         if imp.find_module has been called. |         if PathFinder.find_spec has been called. | ||||||
|         """ |         """ | ||||||
|         import imp |         import importlib.machinery | ||||||
| 
 | 
 | ||||||
|         self.find_module_calls = [] |         self.find_spec_calls = [] | ||||||
|         self.initial_paths = set() |         self.initial_paths = set() | ||||||
| 
 | 
 | ||||||
|         class StubSession: |         class StubSession: | ||||||
|  | @ -1214,22 +1223,22 @@ class TestEarlyRewriteBailout: | ||||||
|             def isinitpath(self, p): |             def isinitpath(self, p): | ||||||
|                 return p in self._initialpaths |                 return p in self._initialpaths | ||||||
| 
 | 
 | ||||||
|         def spy_imp_find_module(name, path): |         def spy_find_spec(name, path): | ||||||
|             self.find_module_calls.append(name) |             self.find_spec_calls.append(name) | ||||||
|             return imp.find_module(name, path) |             return importlib.machinery.PathFinder.find_spec(name, path) | ||||||
| 
 | 
 | ||||||
|         hook = AssertionRewritingHook(pytestconfig) |         hook = AssertionRewritingHook(pytestconfig) | ||||||
|         # use default patterns, otherwise we inherit pytest's testing config |         # use default patterns, otherwise we inherit pytest's testing config | ||||||
|         hook.fnpats[:] = ["test_*.py", "*_test.py"] |         hook.fnpats[:] = ["test_*.py", "*_test.py"] | ||||||
|         monkeypatch.setattr(hook, "_imp_find_module", spy_imp_find_module) |         monkeypatch.setattr(hook, "_find_spec", spy_find_spec) | ||||||
|         hook.set_session(StubSession()) |         hook.set_session(StubSession()) | ||||||
|         testdir.syspathinsert() |         testdir.syspathinsert() | ||||||
|         return hook |         return hook | ||||||
| 
 | 
 | ||||||
|     def test_basic(self, testdir, hook): |     def test_basic(self, testdir, hook): | ||||||
|         """ |         """ | ||||||
|         Ensure we avoid calling imp.find_module when we know for sure a certain module will not be rewritten |         Ensure we avoid calling PathFinder.find_spec when we know for sure a certain | ||||||
|         to optimize assertion rewriting (#3918). |         module will not be rewritten to optimize assertion rewriting (#3918). | ||||||
|         """ |         """ | ||||||
|         testdir.makeconftest( |         testdir.makeconftest( | ||||||
|             """ |             """ | ||||||
|  | @ -1244,24 +1253,24 @@ class TestEarlyRewriteBailout: | ||||||
|         self.initial_paths.add(foobar_path) |         self.initial_paths.add(foobar_path) | ||||||
| 
 | 
 | ||||||
|         # conftest files should always be rewritten |         # conftest files should always be rewritten | ||||||
|         assert hook.find_module("conftest") is not None |         assert hook.find_spec("conftest") is not None | ||||||
|         assert self.find_module_calls == ["conftest"] |         assert self.find_spec_calls == ["conftest"] | ||||||
| 
 | 
 | ||||||
|         # files matching "python_files" mask should always be rewritten |         # files matching "python_files" mask should always be rewritten | ||||||
|         assert hook.find_module("test_foo") is not None |         assert hook.find_spec("test_foo") is not None | ||||||
|         assert self.find_module_calls == ["conftest", "test_foo"] |         assert self.find_spec_calls == ["conftest", "test_foo"] | ||||||
| 
 | 
 | ||||||
|         # file does not match "python_files": early bailout |         # file does not match "python_files": early bailout | ||||||
|         assert hook.find_module("bar") is None |         assert hook.find_spec("bar") is None | ||||||
|         assert self.find_module_calls == ["conftest", "test_foo"] |         assert self.find_spec_calls == ["conftest", "test_foo"] | ||||||
| 
 | 
 | ||||||
|         # file is an initial path (passed on the command-line): should be rewritten |         # file is an initial path (passed on the command-line): should be rewritten | ||||||
|         assert hook.find_module("foobar") is not None |         assert hook.find_spec("foobar") is not None | ||||||
|         assert self.find_module_calls == ["conftest", "test_foo", "foobar"] |         assert self.find_spec_calls == ["conftest", "test_foo", "foobar"] | ||||||
| 
 | 
 | ||||||
|     def test_pattern_contains_subdirectories(self, testdir, hook): |     def test_pattern_contains_subdirectories(self, testdir, hook): | ||||||
|         """If one of the python_files patterns contain subdirectories ("tests/**.py") we can't bailout early |         """If one of the python_files patterns contain subdirectories ("tests/**.py") we can't bailout early | ||||||
|         because we need to match with the full path, which can only be found by calling imp.find_module. |         because we need to match with the full path, which can only be found by calling PathFinder.find_spec | ||||||
|         """ |         """ | ||||||
|         p = testdir.makepyfile( |         p = testdir.makepyfile( | ||||||
|             **{ |             **{ | ||||||
|  | @ -1273,8 +1282,8 @@ class TestEarlyRewriteBailout: | ||||||
|         ) |         ) | ||||||
|         testdir.syspathinsert(p.dirpath()) |         testdir.syspathinsert(p.dirpath()) | ||||||
|         hook.fnpats[:] = ["tests/**.py"] |         hook.fnpats[:] = ["tests/**.py"] | ||||||
|         assert hook.find_module("file") is not None |         assert hook.find_spec("file") is not None | ||||||
|         assert self.find_module_calls == ["file"] |         assert self.find_spec_calls == ["file"] | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.skipif( |     @pytest.mark.skipif( | ||||||
|         sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" |         sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | import os.path | ||||||
| import sys | import sys | ||||||
| 
 | 
 | ||||||
| import py | import py | ||||||
|  | @ -53,6 +54,10 @@ class TestPort: | ||||||
|     def test_matching(self, match, pattern, path): |     def test_matching(self, match, pattern, path): | ||||||
|         assert match(pattern, path) |         assert match(pattern, path) | ||||||
| 
 | 
 | ||||||
|  |     def test_matching_abspath(self, match): | ||||||
|  |         abspath = os.path.abspath(os.path.join("tests/foo.py")) | ||||||
|  |         assert match("tests/foo.py", abspath) | ||||||
|  | 
 | ||||||
|     @pytest.mark.parametrize( |     @pytest.mark.parametrize( | ||||||
|         "pattern, path", |         "pattern, path", | ||||||
|         [ |         [ | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue