Skip to content

Commit ebc2622

Browse files
committed
Improve tests
1 parent 173fd6e commit ebc2622

File tree

2 files changed

+108
-23
lines changed

2 files changed

+108
-23
lines changed

src/_pytest/pathlib.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,7 @@ def import_path(
492492
raise ImportError(path)
493493

494494
if mode is ImportMode.importlib:
495-
relative_path = path.relative_to(root)
496-
module_name = ".".join(relative_path.with_suffix("").parts)
495+
module_name = module_name_from_path(path, root)
497496

498497
for meta_importer in sys.meta_path:
499498
spec = meta_importer.find_spec(module_name, [str(path.parent)])
@@ -573,6 +572,27 @@ def _is_same(f1: str, f2: str) -> bool:
573572
return os.path.samefile(f1, f2)
574573

575574

575+
def module_name_from_path(path: Path, root: Path) -> str:
576+
"""
577+
Return a dotted module name based on the given path, anchored on root.
578+
579+
For example: path="projects/src/tests/test_foo.py" and root="/projects", the
580+
resulting module name will be "src.tests.test_foo".
581+
"""
582+
path = path.with_suffix("")
583+
try:
584+
relative_path = path.relative_to(root)
585+
except ValueError:
586+
# If we can't get a relative path to root, use the full path, except
587+
# for the first part ("d:\\" or "/" depending on the platform, for example).
588+
path_parts = path.parts[1:]
589+
else:
590+
# Use the parts for the relative path to the root path.
591+
path_parts = relative_path.parts
592+
593+
return ".".join(path_parts)
594+
595+
576596
def resolve_package_path(path: Path) -> Optional[Path]:
577597
"""Return the Python package path by looking for the last
578598
directory upwards which still contains an __init__.py.

testing/test_pathlib.py

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import unittest.mock
44
from pathlib import Path
55
from textwrap import dedent
6+
from typing import Any
67

78
import py
89

@@ -17,6 +18,7 @@
1718
from _pytest.pathlib import import_path
1819
from _pytest.pathlib import ImportPathMismatchError
1920
from _pytest.pathlib import maybe_delete_a_numbered_dir
21+
from _pytest.pathlib import module_name_from_path
2022
from _pytest.pathlib import resolve_package_path
2123
from _pytest.pathlib import symlink_or_skip
2224
from _pytest.pathlib import visit
@@ -441,46 +443,109 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N
441443

442444

443445
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
444-
def test_importmode_importlib_with_dataclass(tmpdir):
445-
"""Ensure that importlib mode works with a module containing dataclasses"""
446-
tmpdir.join("src/tests").ensure_dir()
447-
fn = tmpdir.join("src/tests/test_dataclass.py")
448-
fn.write(
446+
def test_importmode_importlib_with_dataclass(tmp_path: Path) -> None:
447+
"""Ensure that importlib mode works with a module containing dataclasses (#7856)."""
448+
fn = tmp_path.joinpath("src/tests/test_dataclass.py")
449+
fn.parent.mkdir(parents=True)
450+
fn.write_text(
449451
dedent(
450452
"""
451453
from dataclasses import dataclass
452454
453455
@dataclass
454-
class DataClass:
456+
class Data:
455457
value: str
458+
"""
459+
)
460+
)
461+
462+
module = import_path(fn, mode="importlib", root=tmp_path)
463+
Data: Any = module.Data # type: ignore[attr-defined]
464+
data = Data(value="foo")
465+
assert data.value == "foo"
466+
assert data.__module__ == "src.tests.test_dataclass"
456467

457-
def test_dataclass():
458-
assert DataClass(value='test').value == 'test'
468+
469+
def test_importmode_importlib_with_pickle(tmp_path: Path) -> None:
470+
"""Ensure that importlib mode works with pickle (#7859)."""
471+
fn = tmp_path.joinpath("src/tests/test_pickle.py")
472+
fn.parent.mkdir(parents=True)
473+
fn.write_text(
474+
dedent(
475+
"""
476+
import pickle
477+
478+
def _action():
479+
return 42
480+
481+
def round_trip():
482+
s = pickle.dumps(_action)
483+
return pickle.loads(s)
459484
"""
460485
)
461486
)
462487

463-
module = import_path(fn, mode="importlib", root=Path(tmpdir))
464-
module.test_dataclass() # type: ignore[attr-defined]
488+
module = import_path(fn, mode="importlib", root=tmp_path)
489+
action: Any = module.round_trip() # type: ignore[attr-defined]
490+
assert action() == 42
465491

466492

467-
def test_importmode_importlib_with_pickle(tmpdir):
468-
"""Ensure that importlib mode works with pickle"""
469-
tmpdir.join("src/tests").ensure_dir()
470-
fn = tmpdir.join("src/tests/test_pickle.py")
471-
fn.write(
493+
def test_importmode_importlib_with_pickle_separate_modules(tmp_path: Path) -> None:
494+
"""
495+
Ensure that importlib mode works can load pickles that look similar but are
496+
defined in separate modules.
497+
"""
498+
fn1 = tmp_path.joinpath("src/m1/tests/test.py")
499+
fn1.parent.mkdir(parents=True)
500+
fn1.write_text(
472501
dedent(
473502
"""
503+
import attr
474504
import pickle
475505
476-
def do_action():
477-
pass
506+
@attr.s(auto_attribs=True)
507+
class Data:
508+
x: int = 42
509+
"""
510+
)
511+
)
512+
513+
fn2 = tmp_path.joinpath("src/m2/tests/test.py")
514+
fn2.parent.mkdir(parents=True)
515+
fn2.write_text(
516+
dedent(
517+
"""
518+
import attr
519+
import pickle
478520
479-
def test_pickle():
480-
pickle.dumps(do_action)
521+
@attr.s(auto_attribs=True)
522+
class Data:
523+
x: str = ""
481524
"""
482525
)
483526
)
484527

485-
module = import_path(fn, mode="importlib", root=Path(tmpdir))
486-
module.test_pickle() # type: ignore[attr-defined]
528+
import pickle
529+
530+
def round_trip(obj):
531+
s = pickle.dumps(obj)
532+
return pickle.loads(s)
533+
534+
module = import_path(fn1, mode="importlib", root=tmp_path)
535+
Data1 = module.Data # type: ignore[attr-defined]
536+
537+
module = import_path(fn2, mode="importlib", root=tmp_path)
538+
Data2 = module.Data # type: ignore[attr-defined]
539+
540+
assert round_trip(Data1(20)) == Data1(20)
541+
assert round_trip(Data2("hello")) == Data2("hello")
542+
assert Data1.__module__ == "src.m1.tests.test"
543+
assert Data2.__module__ == "src.m2.tests.test"
544+
545+
546+
def test_module_name_from_path(tmp_path: Path) -> None:
547+
result = module_name_from_path(tmp_path / "src/tests/test_foo.py", tmp_path)
548+
assert result == "src.tests.test_foo"
549+
550+
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
551+
assert result == "home.foo.test_foo"

0 commit comments

Comments
 (0)