module __file__ attribute does not have the right casing
This commit is contained in:
parent
9af6d46371
commit
cfc03a2852
|
@ -0,0 +1,20 @@
|
||||||
|
import os
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def module_casesensitivepath(module: ModuleType) -> Optional[str]:
|
||||||
|
"""Return the canonical __file__ of the module without resolving symlinks."""
|
||||||
|
path = module.__file__
|
||||||
|
if path is None:
|
||||||
|
return None
|
||||||
|
return casesensitivepath(path)
|
||||||
|
|
||||||
|
|
||||||
|
def casesensitivepath(path: str) -> str:
|
||||||
|
"""Return the case-sensitive version of the path."""
|
||||||
|
resolved_path = os.path.realpath(path)
|
||||||
|
if resolved_path.lower() == path.lower():
|
||||||
|
return resolved_path
|
||||||
|
# Patch has one or more symlinks. Todo: find the correct path casing.
|
||||||
|
return path
|
|
@ -30,6 +30,7 @@ from typing import overload
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from . import error
|
from . import error
|
||||||
|
from . import os_path
|
||||||
|
|
||||||
# Moved from local.py.
|
# Moved from local.py.
|
||||||
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
||||||
|
@ -1126,7 +1127,7 @@ class LocalPath:
|
||||||
if self.basename == "__init__.py":
|
if self.basename == "__init__.py":
|
||||||
return mod # we don't check anything as we might
|
return mod # we don't check anything as we might
|
||||||
# be in a namespace package ... too icky to check
|
# be in a namespace package ... too icky to check
|
||||||
modfile = mod.__file__
|
modfile = os_path.module_casesensitivepath(mod)
|
||||||
assert modfile is not None
|
assert modfile is not None
|
||||||
if modfile[-4:] in (".pyc", ".pyo"):
|
if modfile[-4:] in (".pyc", ".pyo"):
|
||||||
modfile = modfile[:-1]
|
modfile = modfile[:-1]
|
||||||
|
|
|
@ -52,6 +52,7 @@ from .findpaths import determine_setup
|
||||||
from _pytest._code import ExceptionInfo
|
from _pytest._code import ExceptionInfo
|
||||||
from _pytest._code import filter_traceback
|
from _pytest._code import filter_traceback
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
|
from _pytest._py.os_path import module_casesensitivepath
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
|
@ -631,8 +632,7 @@ class PytestPluginManager(PluginManager):
|
||||||
def _importconftest(
|
def _importconftest(
|
||||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||||
) -> types.ModuleType:
|
) -> types.ModuleType:
|
||||||
conftestpath_plugin_name = str(conftestpath)
|
existing = self.get_plugin(str(conftestpath))
|
||||||
existing = self.get_plugin(conftestpath_plugin_name)
|
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
return cast(types.ModuleType, existing)
|
return cast(types.ModuleType, existing)
|
||||||
|
|
||||||
|
@ -668,7 +668,7 @@ class PytestPluginManager(PluginManager):
|
||||||
)
|
)
|
||||||
mods.append(mod)
|
mods.append(mod)
|
||||||
self.trace(f"loading conftestmodule {mod!r}")
|
self.trace(f"loading conftestmodule {mod!r}")
|
||||||
self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
|
self.consider_conftest(mod)
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
def _check_non_top_pytest_plugins(
|
def _check_non_top_pytest_plugins(
|
||||||
|
@ -748,11 +748,9 @@ class PytestPluginManager(PluginManager):
|
||||||
del self._name2plugin["pytest_" + name]
|
del self._name2plugin["pytest_" + name]
|
||||||
self.import_plugin(arg, consider_entry_points=True)
|
self.import_plugin(arg, consider_entry_points=True)
|
||||||
|
|
||||||
def consider_conftest(
|
def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
|
||||||
self, conftestmodule: types.ModuleType, registration_name: str
|
|
||||||
) -> None:
|
|
||||||
""":meta private:"""
|
""":meta private:"""
|
||||||
self.register(conftestmodule, name=registration_name)
|
self.register(conftestmodule, name=module_casesensitivepath(conftestmodule))
|
||||||
|
|
||||||
def consider_env(self) -> None:
|
def consider_env(self) -> None:
|
||||||
""":meta private:"""
|
""":meta private:"""
|
||||||
|
|
|
@ -37,6 +37,7 @@ from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import FormattedExcinfo
|
from _pytest._code.code import FormattedExcinfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
|
from _pytest._py.os_path import casesensitivepath
|
||||||
from _pytest.compat import _PytestWrapper
|
from _pytest.compat import _PytestWrapper
|
||||||
from _pytest.compat import assert_never
|
from _pytest.compat import assert_never
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
|
@ -1485,17 +1486,22 @@ class FixtureManager:
|
||||||
|
|
||||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
||||||
nodeid = None
|
nodeid = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
|
module_file: str = plugin.__file__ # type: ignore[attr-defined]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
module_absfile = Path(casesensitivepath(str(absolutepath(module_file))))
|
||||||
|
|
||||||
# Construct the base nodeid which is later used to check
|
# Construct the base nodeid which is later used to check
|
||||||
# what fixtures are visible for particular tests (as denoted
|
# what fixtures are visible for particular tests (as denoted
|
||||||
# by their test id).
|
# by their test id).
|
||||||
if p.name == "conftest.py":
|
if module_absfile.name == "conftest.py":
|
||||||
try:
|
try:
|
||||||
nodeid = str(p.parent.relative_to(self.config.rootpath))
|
nodeid = str(
|
||||||
|
module_absfile.parent.relative_to(self.config.rootpath)
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
nodeid = ""
|
nodeid = ""
|
||||||
if nodeid == ".":
|
if nodeid == ".":
|
||||||
|
|
|
@ -8,6 +8,7 @@ from typing import Optional
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest._py.os_path import module_casesensitivepath
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import PrintHelp
|
from _pytest.config import PrintHelp
|
||||||
|
@ -265,7 +266,7 @@ def pytest_report_header(config: Config) -> List[str]:
|
||||||
items = config.pluginmanager.list_name_plugin()
|
items = config.pluginmanager.list_name_plugin()
|
||||||
for name, plugin in items:
|
for name, plugin in items:
|
||||||
if hasattr(plugin, "__file__"):
|
if hasattr(plugin, "__file__"):
|
||||||
r = plugin.__file__
|
r = module_casesensitivepath(plugin)
|
||||||
else:
|
else:
|
||||||
r = repr(plugin)
|
r = repr(plugin)
|
||||||
lines.append(f" {name:<20}: {r}")
|
lines.append(f" {name:<20}: {r}")
|
||||||
|
|
|
@ -35,6 +35,7 @@ from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from _pytest._py.os_path import module_casesensitivepath
|
||||||
from _pytest.compat import assert_never
|
from _pytest.compat import assert_never
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
@ -572,7 +573,7 @@ def import_path(
|
||||||
|
|
||||||
ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
|
ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
|
||||||
if ignore != "1":
|
if ignore != "1":
|
||||||
module_file = mod.__file__
|
module_file = module_casesensitivepath(mod)
|
||||||
if module_file is None:
|
if module_file is None:
|
||||||
raise ImportPathMismatchError(module_name, module_file, path)
|
raise ImportPathMismatchError(module_name, module_file, path)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
from _pytest._py import os_path
|
||||||
|
|
||||||
|
_ON_CASEINSENSITIVE_OS = sys.platform.startswith("win")
|
||||||
|
|
||||||
|
|
||||||
|
def test_casesensitivepath(tmp_path: Path) -> None:
|
||||||
|
dirname_with_caps = tmp_path / "Testdir"
|
||||||
|
dirname_with_caps.mkdir()
|
||||||
|
real_filename = dirname_with_caps / "_test_casesensitivepath.py"
|
||||||
|
with real_filename.open("wb"):
|
||||||
|
pass
|
||||||
|
real_linkname = dirname_with_caps / "_test_casesensitivepath_link.py"
|
||||||
|
real_linkname.symlink_to(real_filename)
|
||||||
|
|
||||||
|
# Test path resolving
|
||||||
|
|
||||||
|
original = str(real_filename)
|
||||||
|
expected = str(real_filename)
|
||||||
|
assert os_path.casesensitivepath(original) == expected
|
||||||
|
|
||||||
|
original = str(real_filename).lower()
|
||||||
|
if _ON_CASEINSENSITIVE_OS:
|
||||||
|
expected = str(real_filename)
|
||||||
|
else:
|
||||||
|
expected = str(real_filename).lower()
|
||||||
|
assert os_path.casesensitivepath(original) == expected
|
||||||
|
|
||||||
|
# Test symlink preservation
|
||||||
|
|
||||||
|
original = str(real_linkname)
|
||||||
|
expected = str(real_linkname)
|
||||||
|
assert os_path.casesensitivepath(original) == expected
|
||||||
|
|
||||||
|
original = str(real_linkname).lower()
|
||||||
|
expected = str(real_linkname).lower()
|
||||||
|
assert os_path.casesensitivepath(original) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_casesensitivepath(tmp_path: Path) -> None:
|
||||||
|
dirname_with_caps = tmp_path / "Testdir"
|
||||||
|
dirname_with_caps.mkdir()
|
||||||
|
real_filename = dirname_with_caps / "_test_module_casesensitivepath.py"
|
||||||
|
with real_filename.open("wb"):
|
||||||
|
pass
|
||||||
|
real_linkname = dirname_with_caps / "_test_module_casesensitivepath_link.py"
|
||||||
|
real_linkname.symlink_to(real_filename)
|
||||||
|
|
||||||
|
mod = ModuleType("dummy.name")
|
||||||
|
|
||||||
|
mod.__file__ = None
|
||||||
|
assert os_path.module_casesensitivepath(mod) is None
|
||||||
|
|
||||||
|
# Test path resolving
|
||||||
|
|
||||||
|
original = str(real_filename)
|
||||||
|
expected = str(real_filename)
|
||||||
|
mod.__file__ = original
|
||||||
|
assert os_path.module_casesensitivepath(mod) == expected
|
||||||
|
|
||||||
|
original = str(real_filename).lower()
|
||||||
|
if _ON_CASEINSENSITIVE_OS:
|
||||||
|
expected = str(real_filename)
|
||||||
|
else:
|
||||||
|
expected = str(real_filename).lower()
|
||||||
|
mod.__file__ = original
|
||||||
|
assert os_path.module_casesensitivepath(mod) == expected
|
||||||
|
|
||||||
|
# Test symlink preservation
|
||||||
|
|
||||||
|
original = str(real_linkname)
|
||||||
|
expected = str(real_linkname)
|
||||||
|
mod.__file__ = original
|
||||||
|
assert os_path.module_casesensitivepath(mod) == expected
|
||||||
|
|
||||||
|
original = str(real_linkname).lower()
|
||||||
|
expected = str(real_linkname).lower()
|
||||||
|
mod.__file__ = original
|
||||||
|
assert os_path.module_casesensitivepath(mod) == expected
|
|
@ -118,17 +118,14 @@ class TestPytestPluginInteractions:
|
||||||
plugin = config.pluginmanager.get_plugin(str(conftest))
|
plugin = config.pluginmanager.get_plugin(str(conftest))
|
||||||
assert plugin is mod
|
assert plugin is mod
|
||||||
|
|
||||||
mod_uppercase = config.pluginmanager._importconftest(
|
with pytest.raises(ValueError, match="Plugin name already registered"):
|
||||||
conftest_upper_case,
|
config.pluginmanager._importconftest(
|
||||||
importmode="prepend",
|
conftest_upper_case,
|
||||||
rootpath=pytester.path,
|
importmode="prepend",
|
||||||
)
|
rootpath=pytester.path,
|
||||||
|
)
|
||||||
plugin_uppercase = config.pluginmanager.get_plugin(str(conftest_upper_case))
|
plugin_uppercase = config.pluginmanager.get_plugin(str(conftest_upper_case))
|
||||||
assert plugin_uppercase is mod_uppercase
|
assert plugin_uppercase is None
|
||||||
|
|
||||||
# No str(conftestpath) normalization so conftest should be imported
|
|
||||||
# twice and modules should be different objects
|
|
||||||
assert mod is not mod_uppercase
|
|
||||||
|
|
||||||
def test_hook_tracing(self, _config_for_test: Config) -> None:
|
def test_hook_tracing(self, _config_for_test: Config) -> None:
|
||||||
pytestpm = _config_for_test.pluginmanager # fully initialized with plugins
|
pytestpm = _config_for_test.pluginmanager # fully initialized with plugins
|
||||||
|
@ -400,7 +397,7 @@ class TestPytestPluginManager:
|
||||||
pytester.makepyfile("pytest_plugins='xyz'"), root=pytester.path
|
pytester.makepyfile("pytest_plugins='xyz'"), root=pytester.path
|
||||||
)
|
)
|
||||||
with pytest.raises(ImportError):
|
with pytest.raises(ImportError):
|
||||||
pytestpm.consider_conftest(mod, registration_name="unused")
|
pytestpm.consider_conftest(mod)
|
||||||
|
|
||||||
|
|
||||||
class TestPytestPluginManagerBootstrapming:
|
class TestPytestPluginManagerBootstrapming:
|
||||||
|
|
Loading…
Reference in New Issue