diff --git a/src/django_components/component_media.py b/src/django_components/component_media.py index dc6d7091..0663e44e 100644 --- a/src/django_components/component_media.py +++ b/src/django_components/component_media.py @@ -226,7 +226,8 @@ def _normalize_media_filepath(filepath: Any) -> Union[str, SafeData]: return filepath if isinstance(filepath, (Path, os.PathLike)) or hasattr(filepath, "__fspath__"): - filepath = filepath.__fspath__() + # In case of Windows OS, convert to forward slashes + filepath = Path(filepath.__fspath__()).as_posix() if isinstance(filepath, bytes): filepath = filepath.decode("utf-8") @@ -293,19 +294,21 @@ def _resolve_component_relative_files(attrs: MutableMapping) -> None: # If not, don't modify anything. def resolve_file(filepath: Union[str, SafeData]) -> Union[str, SafeData]: if isinstance(filepath, str): - maybe_resolved_filepath = os.path.join(comp_dir_abs, filepath) - component_import_filepath = os.path.join(comp_dir_rel, filepath) + filepath_abs = os.path.join(comp_dir_abs, filepath) + # NOTE: The paths to resources need to use POSIX (forward slashes) for Django to wor + # See https://github.com/EmilStenstrom/django-components/issues/796 + filepath_rel_to_comp_dir = Path(os.path.join(comp_dir_rel, filepath)).as_posix() - if os.path.isfile(maybe_resolved_filepath): + if os.path.isfile(filepath_abs): # NOTE: It's important to use `repr`, so we don't trigger __str__ on SafeStrings logger.debug( f"Interpreting template '{repr(filepath)}' of component '{module_name}'" " relatively to component file" ) - return component_import_filepath - return filepath + return filepath_rel_to_comp_dir + # If resolved absolute path does NOT exist or filepath is NOT a string, then return as is logger.debug( f"Interpreting template '{repr(filepath)}' of component '{module_name}'" " relatively to components directory" diff --git a/src/django_components/util/loader.py b/src/django_components/util/loader.py index 672aceb5..5d8a601d 100644 --- a/src/django_components/util/loader.py +++ b/src/django_components/util/loader.py @@ -1,6 +1,6 @@ import glob import os -from pathlib import Path +from pathlib import Path, PurePosixPath, PureWindowsPath from typing import List, NamedTuple, Optional, Set, Union from django.apps import apps @@ -211,13 +211,11 @@ def _filepath_to_python_module( - Then the path relative to project root is `app/components/mycomp.py` - Which we then turn into python import path `app.components.mycomp` """ - rel_path = os.path.relpath(file_path, start=root_fs_path) - rel_path_without_suffix = str(Path(rel_path).with_suffix("")) + path_cls = PureWindowsPath if os.name == "nt" else PurePosixPath - # NOTE: `Path` normalizes paths to use `/` as separator, while `os.path` - # uses `os.path.sep`. - sep = os.path.sep if os.path.sep in rel_path_without_suffix else "/" - module_name = rel_path_without_suffix.replace(sep, ".") + rel_path = path_cls(file_path).relative_to(path_cls(root_fs_path)) + rel_path_parts = rel_path.with_suffix("").parts + module_name = ".".join(rel_path_parts) # Combine with the base module path full_module_name = f"{root_module_path}.{module_name}" if root_module_path else module_name diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index d9080515..34248e7b 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -1,5 +1,6 @@ import functools import subprocess +import sys import time from pathlib import Path @@ -39,12 +40,13 @@ def run_django_dev_server(): """Fixture to run Django development server in the background.""" # Get the path where testserver is defined, so the command doesn't depend # on user's current working directory. - testserver_dir = (Path(__file__).parent / "testserver").absolute() + testserver_dir = (Path(__file__).parent / "testserver").resolve() # Start the Django dev server in the background print("Starting Django dev server...") proc = subprocess.Popen( - ["python", "manage.py", "runserver", f"127.0.0.1:{TEST_SERVER_PORT}", "--noreload"], + # NOTE: Use `sys.executable` so this works both for Unix and Windows OS + [sys.executable, "manage.py", "runserver", f"127.0.0.1:{TEST_SERVER_PORT}", "--noreload"], cwd=testserver_dir, ) diff --git a/tests/test_finders.py b/tests/test_finders.py index e8a267bb..bfbf78ab 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -58,6 +58,9 @@ def do_collect(): post_process=True, ) collected = cmd.collect() + + # Convert collected paths from string to Path, so we can run tests on both Unix and Windows + collected = {key: [Path(item) for item in items] for key, items in collected.items()} return collected @@ -86,10 +89,10 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that the component files are NOT loaded when our finder is NOT added - self.assertNotIn("staticfiles/staticfiles.css", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.js", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.html", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.py", collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.css"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.js"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.html"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.py"), collected["modified"]) self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["post_processed"], []) @@ -109,10 +112,10 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that our staticfiles_finder finds the files and OMITS .py and .html files - self.assertIn("staticfiles/staticfiles.css", collected["modified"]) - self.assertIn("staticfiles/staticfiles.js", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.html", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.py", collected["modified"]) + self.assertIn(Path("staticfiles/staticfiles.css"), collected["modified"]) + self.assertIn(Path("staticfiles/staticfiles.js"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.html"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.py"), collected["modified"]) self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["post_processed"], []) @@ -138,10 +141,10 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that our staticfiles_finder finds the files and OMITS .py and .html files - self.assertNotIn("staticfiles/staticfiles.css", collected["modified"]) - self.assertIn("staticfiles/staticfiles.js", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.html", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.py", collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.css"), collected["modified"]) + self.assertIn(Path("staticfiles/staticfiles.js"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.html"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.py"), collected["modified"]) self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["post_processed"], []) @@ -169,10 +172,10 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that our staticfiles_finder finds the files and OMITS .py and .html files - self.assertIn("staticfiles/staticfiles.css", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.js", collected["modified"]) - self.assertIn("staticfiles/staticfiles.html", collected["modified"]) - self.assertIn("staticfiles/staticfiles.py", collected["modified"]) + self.assertIn(Path("staticfiles/staticfiles.css"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.js"), collected["modified"]) + self.assertIn(Path("staticfiles/staticfiles.html"), collected["modified"]) + self.assertIn(Path("staticfiles/staticfiles.py"), collected["modified"]) self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["post_processed"], []) @@ -201,10 +204,10 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that our staticfiles_finder finds the files and OMITS .py and .html files - self.assertIn("staticfiles/staticfiles.css", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.js", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.html", collected["modified"]) - self.assertNotIn("staticfiles/staticfiles.py", collected["modified"]) + self.assertIn(Path("staticfiles/staticfiles.css"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.js"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.html"), collected["modified"]) + self.assertNotIn(Path("staticfiles/staticfiles.py"), collected["modified"]) self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["post_processed"], []) diff --git a/tests/test_loader.py b/tests/test_loader.py index 6babbe01..1b6168ad 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,5 +1,4 @@ import os -import re from pathlib import Path from unittest.mock import MagicMock, patch @@ -35,8 +34,10 @@ class ComponentDirsTest(BaseTestCase): # Apps with a `components` dir self.assertEqual(len(apps_dirs), 2) - self.assertRegex(str(apps_dirs[0]), re.compile(r"\/django_components\/components$")) - self.assertRegex(str(apps_dirs[1]), re.compile(r"\/tests\/test_app\/components$")) + + # NOTE: Compare parts so that the test works on Windows too + self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) + self.assertTupleEqual(apps_dirs[1].parts[-3:], ("tests", "test_app", "components")) @override_settings( BASE_DIR=Path(__file__).parent.resolve() / "test_structures" / "test_structure_1", # noqa @@ -49,8 +50,10 @@ class ComponentDirsTest(BaseTestCase): # Apps with a `components` dir self.assertEqual(len(apps_dirs), 2) - self.assertRegex(str(apps_dirs[0]), re.compile(r"\/django_components\/components$")) - self.assertRegex(str(apps_dirs[1]), re.compile(r"\/tests\/test_app\/components$")) + + # NOTE: Compare parts so that the test works on Windows too + self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) + self.assertTupleEqual(apps_dirs[1].parts[-3:], ("tests", "test_app", "components")) expected = [ Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components", @@ -76,8 +79,10 @@ class ComponentDirsTest(BaseTestCase): # Apps with a `components` dir self.assertEqual(len(apps_dirs), 2) - self.assertRegex(str(apps_dirs[0]), re.compile(r"\/django_components\/components$")) - self.assertRegex(str(apps_dirs[1]), re.compile(r"\/tests\/test_app\/components$")) + + # NOTE: Compare parts so that the test works on Windows too + self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) + self.assertTupleEqual(apps_dirs[1].parts[-3:], ("tests", "test_app", "components")) self.assertEqual( own_dirs, @@ -104,8 +109,10 @@ class ComponentDirsTest(BaseTestCase): # Apps with a `components` dir self.assertEqual(len(apps_dirs), 2) - self.assertRegex(str(apps_dirs[0]), re.compile(r"\/django_components\/components$")) - self.assertRegex(str(apps_dirs[1]), re.compile(r"\/tests\/test_app\/components$")) + + # NOTE: Compare parts so that the test works on Windows too + self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) + self.assertTupleEqual(apps_dirs[1].parts[-3:], ("tests", "test_app", "components")) @override_settings( BASE_DIR=Path(__file__).parent.resolve(), @@ -141,7 +148,9 @@ class ComponentDirsTest(BaseTestCase): # Apps with a `components` dir self.assertEqual(len(apps_dirs), 1) - self.assertRegex(str(apps_dirs[0]), re.compile(r"\/tests\/test_app\/custom_comps_dir$")) + + # NOTE: Compare parts so that the test works on Windows too + self.assertTupleEqual(apps_dirs[0].parts[-3:], ("tests", "test_app", "custom_comps_dir")) self.assertEqual( own_dirs, @@ -204,8 +213,10 @@ class ComponentDirsTest(BaseTestCase): # Apps with a `components` dir self.assertEqual(len(apps_dirs), 2) - self.assertRegex(str(apps_dirs[0]), re.compile(r"\/django_components\/components$")) - self.assertRegex(str(apps_dirs[1]), re.compile(r"\/tests\/test_app_nested\/app\/components$")) + + # NOTE: Compare parts so that the test works on Windows too + self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) + self.assertTupleEqual(apps_dirs[1].parts[-4:], ("tests", "test_app_nested", "app", "components")) self.assertEqual( own_dirs, @@ -225,7 +236,7 @@ class ComponentFilesTest(BaseTestCase): files = sorted(get_component_files(".py")) dot_paths = [f.dot_path for f in files] - file_paths = [str(f.filepath) for f in files] + file_paths = [f.filepath for f in files] self.assertEqual( dot_paths, @@ -243,20 +254,20 @@ class ComponentFilesTest(BaseTestCase): ], ) - self.assertEqual( - [ - file_paths[0].endswith("tests/components/__init__.py"), - file_paths[1].endswith("tests/components/multi_file/multi_file.py"), - file_paths[2].endswith("tests/components/relative_file/relative_file.py"), - file_paths[3].endswith("tests/components/relative_file_pathobj/relative_file_pathobj.py"), - file_paths[4].endswith("tests/components/single_file.py"), - file_paths[5].endswith("tests/components/staticfiles/staticfiles.py"), - file_paths[6].endswith("tests/components/urls.py"), - file_paths[7].endswith("django_components/components/__init__.py"), - file_paths[8].endswith("django_components/components/dynamic.py"), - file_paths[9].endswith("tests/test_app/components/app_lvl_comp/app_lvl_comp.py"), - ], - [True for _ in range(len(file_paths))], + # NOTE: Compare parts so that the test works on Windows too + self.assertTupleEqual(file_paths[0].parts[-3:], ("tests", "components", "__init__.py")) + self.assertTupleEqual(file_paths[1].parts[-4:], ("tests", "components", "multi_file", "multi_file.py")) + self.assertTupleEqual(file_paths[2].parts[-4:], ("tests", "components", "relative_file", "relative_file.py")) + self.assertTupleEqual( + file_paths[3].parts[-4:], ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.py") + ) + self.assertTupleEqual(file_paths[4].parts[-3:], ("tests", "components", "single_file.py")) + self.assertTupleEqual(file_paths[5].parts[-4:], ("tests", "components", "staticfiles", "staticfiles.py")) + self.assertTupleEqual(file_paths[6].parts[-3:], ("tests", "components", "urls.py")) + self.assertTupleEqual(file_paths[7].parts[-3:], ("django_components", "components", "__init__.py")) + self.assertTupleEqual(file_paths[8].parts[-3:], ("django_components", "components", "dynamic.py")) + self.assertTupleEqual( + file_paths[9].parts[-5:], ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py") ) @override_settings( @@ -266,9 +277,7 @@ class ComponentFilesTest(BaseTestCase): files = sorted(get_component_files(".js")) dot_paths = [f.dot_path for f in files] - file_paths = [str(f.filepath) for f in files] - - print(file_paths) + file_paths = [f.filepath for f in files] self.assertEqual( dot_paths, @@ -280,14 +289,14 @@ class ComponentFilesTest(BaseTestCase): ], ) - self.assertEqual( - [ - file_paths[0].endswith("tests/components/relative_file/relative_file.js"), - file_paths[1].endswith("tests/components/relative_file_pathobj/relative_file_pathobj.js"), - file_paths[2].endswith("tests/components/staticfiles/staticfiles.js"), - file_paths[3].endswith("tests/test_app/components/app_lvl_comp/app_lvl_comp.js"), - ], - [True for _ in range(len(file_paths))], + # NOTE: Compare parts so that the test works on Windows too + self.assertTupleEqual(file_paths[0].parts[-4:], ("tests", "components", "relative_file", "relative_file.js")) + self.assertTupleEqual( + file_paths[1].parts[-4:], ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.js") + ) + self.assertTupleEqual(file_paths[2].parts[-4:], ("tests", "components", "staticfiles", "staticfiles.js")) + self.assertTupleEqual( + file_paths[3].parts[-5:], ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.js") ) @@ -307,31 +316,44 @@ class TestFilepathToPythonModule(BaseTestCase): "tests.components.relative_file.relative_file", ) - def test_handles_nonlinux_paths(self): - base_path = str(settings.BASE_DIR).replace("/", "//") + def test_handles_separators_based_on_os_name(self): + base_path = str(settings.BASE_DIR) - with patch("os.path.sep", new="//"): - the_path = os.path.join(base_path, "tests.py") + with patch("os.name", new="posix"): + the_path = base_path + "/" + "tests.py" self.assertEqual( _filepath_to_python_module(the_path, base_path, None), "tests", ) - the_path = os.path.join(base_path, "tests//components//relative_file//relative_file.py") + the_path = base_path + "/" + "tests/components/relative_file/relative_file.py" self.assertEqual( _filepath_to_python_module(the_path, base_path, None), "tests.components.relative_file.relative_file", ) - base_path = str(settings.BASE_DIR).replace("//", "\\") - with patch("os.path.sep", new="\\"): - the_path = os.path.join(base_path, "tests.py") + base_path = str(settings.BASE_DIR).replace("/", "\\") + with patch("os.name", new="nt"): + the_path = base_path + "\\" + "tests.py" self.assertEqual( _filepath_to_python_module(the_path, base_path, None), "tests", ) - the_path = os.path.join(base_path, "tests\\components\\relative_file\\relative_file.py") + the_path = base_path + "\\" + "tests\\components\\relative_file\\relative_file.py" + self.assertEqual( + _filepath_to_python_module(the_path, base_path, None), + "tests.components.relative_file.relative_file", + ) + + # NOTE: Windows should handle also POSIX separator + the_path = base_path + "/" + "tests.py" + self.assertEqual( + _filepath_to_python_module(the_path, base_path, None), + "tests", + ) + + the_path = base_path + "/" + "tests/components/relative_file/relative_file.py" self.assertEqual( _filepath_to_python_module(the_path, base_path, None), "tests.components.relative_file.relative_file",