Skip to content

Commit fc56c76

Browse files
authored
Windows support (#29)
1 parent 26be94e commit fc56c76

File tree

3 files changed

+75
-39
lines changed

3 files changed

+75
-39
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ jobs:
2222
- uses: pre-commit/[email protected]
2323

2424
tests:
25-
runs-on: ubuntu-latest
25+
runs-on: ${{ matrix.os }}
2626
strategy:
2727
fail-fast: false
2828
matrix:
2929
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
30+
os: [ubuntu-latest, windows-latest]
3031

3132
steps:
3233
- uses: actions/checkout@v4
@@ -50,6 +51,7 @@ jobs:
5051
- name: Test with tox
5152
run: tox
5253
- uses: codecov/codecov-action@v4
54+
if: matrix.python-version == '3.8' && matrix.os == 'ubuntu-latest'
5355
with:
5456
token: ${{ secrets.CODECOV_TOKEN }}
5557
verbose: true

tests/test_cli.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77

88
import dirhash
99

10-
console_script = os.path.join(os.path.dirname(sys.executable), "dirhash")
10+
console_script = os.path.join(
11+
os.path.dirname(sys.executable),
12+
"dirhash.exe" if os.name == "nt" else "dirhash",
13+
)
14+
if not os.path.isfile(console_script):
15+
print(os.listdir(os.path.dirname(sys.executable)))
16+
raise FileNotFoundError(f"Could not find console script at {console_script}.")
17+
if not os.access(console_script, os.X_OK):
18+
raise PermissionError(f"Console script at {console_script} is not executable.")
1119

1220

1321
def dirhash_run(argstring, add_env=None):
14-
assert os.path.isfile(console_script)
15-
assert os.access(console_script, os.X_OK)
1622
if add_env:
1723
env = os.environ.copy()
1824
env.update(add_env)
@@ -22,6 +28,7 @@ def dirhash_run(argstring, add_env=None):
2228
[console_script] + shlex.split(argstring),
2329
stdout=subprocess.PIPE,
2430
stderr=subprocess.PIPE,
31+
text=True,
2532
env=env,
2633
)
2734
output, error = process.communicate()
@@ -59,6 +66,13 @@ def create_default_tree(tmpdir):
5966
tmpdir.join("file.ext2").write("file with extension .ext2")
6067

6168

69+
def osp(path: str) -> str:
70+
"""Normalize path for OS."""
71+
if os.name == "nt": # pragma: no cover
72+
return path.replace("/", "\\")
73+
return path
74+
75+
6276
class TestCLI:
6377
@pytest.mark.parametrize(
6478
"argstring, non_default_kwargs",
@@ -171,7 +185,7 @@ def test_list(self, description, argstrings, output, tmpdir):
171185
o, error, returncode = dirhash_run(argstring)
172186
assert returncode == 0
173187
assert error == ""
174-
assert o == output
188+
assert o == osp(output)
175189

176190
@pytest.mark.parametrize(
177191
"argstring, kwargs, expected_hashes",

tests/test_dirhash.py

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@
2121
)
2222

2323

24+
def osp(path: str) -> str:
25+
"""Normalize path for OS."""
26+
if os.name == "nt": # pragma: no cover
27+
return path.replace("/", "\\")
28+
return path
29+
30+
31+
def map_osp(paths):
32+
return [osp(path) for path in paths]
33+
34+
2435
class TestGetHasherFactory:
2536
def test_get_guaranteed(self):
2637
algorithm_and_hasher_factory = [
@@ -142,7 +153,7 @@ def teardown_method(self):
142153
shutil.rmtree(self.dir)
143154

144155
def path_to(self, relpath):
145-
return os.path.join(self.dir, relpath)
156+
return os.path.join(self.dir, osp(relpath))
146157

147158
def mkdirs(self, dirpath):
148159
os.makedirs(self.path_to(dirpath))
@@ -173,7 +184,7 @@ def test_basic(self):
173184
self.mkfile("root/d1/d11/f1")
174185
self.mkfile("root/d2/f1")
175186

176-
expected_filepaths = ["d1/d11/f1", "d1/f1", "d2/f1", "f1"]
187+
expected_filepaths = map_osp(["d1/d11/f1", "d1/f1", "d2/f1", "f1"])
177188
filepaths = included_paths(self.path_to("root"))
178189
assert filepaths == expected_filepaths
179190

@@ -220,11 +231,11 @@ def test_symlinked_dir(self):
220231
assert filepaths == ["f1"]
221232

222233
filepaths = included_paths(self.path_to("root"), linked_dirs=True)
223-
assert filepaths == ["d1/f1", "d1/f2", "f1"]
234+
assert filepaths == map_osp(["d1/f1", "d1/f2", "f1"])
224235

225236
# default is 'linked_dirs': True
226237
filepaths = included_paths(self.path_to("root"))
227-
assert filepaths == ["d1/f1", "d1/f2", "f1"]
238+
assert filepaths == map_osp(["d1/f1", "d1/f2", "f1"])
228239

229240
def test_cyclic_link(self):
230241
self.mkdirs("root/d1")
@@ -237,7 +248,7 @@ def test_cyclic_link(self):
237248
assert str(exc_info.value).startswith("Symlink recursion:")
238249

239250
filepaths = included_paths(self.path_to("root"), allow_cyclic_links=True)
240-
assert filepaths == ["d1/link_back/."]
251+
assert filepaths == map_osp(["d1/link_back/."])
241252

242253
# default is 'allow_cyclic_links': False
243254
with pytest.raises(SymlinkRecursionError):
@@ -255,11 +266,11 @@ def test_ignore_hidden(self):
255266

256267
# no ignore
257268
filepaths = included_paths(self.path_to("root"))
258-
assert filepaths == [".d2/f1", ".f2", "d1/.f2", "d1/f1", "f1"]
269+
assert filepaths == map_osp([".d2/f1", ".f2", "d1/.f2", "d1/f1", "f1"])
259270

260271
# with ignore
261272
filepaths = included_paths(self.path_to("root"), match=["*", "!.*"])
262-
assert filepaths == ["d1/f1", "f1"]
273+
assert filepaths == map_osp(["d1/f1", "f1"])
263274

264275
def test_ignore_hidden_files_only(self):
265276
self.mkdirs("root/d1")
@@ -273,13 +284,13 @@ def test_ignore_hidden_files_only(self):
273284

274285
# no ignore
275286
filepaths = included_paths(self.path_to("root"))
276-
assert filepaths == [".d2/f1", ".f2", "d1/.f2", "d1/f1", "f1"]
287+
assert filepaths == map_osp([".d2/f1", ".f2", "d1/.f2", "d1/f1", "f1"])
277288

278289
# with ignore
279290
filepaths = included_paths(
280291
self.path_to("root"), match=["**/*", "!**/.*", "**/.*/*", "!**/.*/.*"]
281292
)
282-
assert filepaths == [".d2/f1", "d1/f1", "f1"]
293+
assert filepaths == map_osp([".d2/f1", "d1/f1", "f1"])
283294

284295
def test_ignore_hidden_explicitly_recursive(self):
285296
self.mkdirs("root/d1")
@@ -293,11 +304,11 @@ def test_ignore_hidden_explicitly_recursive(self):
293304

294305
# no ignore
295306
filepaths = included_paths(self.path_to("root"))
296-
assert filepaths == [".d2/f1", ".f2", "d1/.f2", "d1/f1", "f1"]
307+
assert filepaths == map_osp([".d2/f1", ".f2", "d1/.f2", "d1/f1", "f1"])
297308

298309
# with ignore
299310
filepaths = included_paths(self.path_to("root"), match=["*", "!**/.*"])
300-
assert filepaths == ["d1/f1", "f1"]
311+
assert filepaths == map_osp(["d1/f1", "f1"])
301312

302313
def test_exclude_hidden_dirs(self):
303314
self.mkdirs("root/d1")
@@ -312,11 +323,13 @@ def test_exclude_hidden_dirs(self):
312323

313324
# no ignore
314325
filepaths = included_paths(self.path_to("root"), empty_dirs=True)
315-
assert filepaths == [".d2/f1", ".f2", "d1/.d1/.", "d1/.f2", "d1/f1", "f1"]
326+
assert filepaths == map_osp(
327+
[".d2/f1", ".f2", "d1/.d1/.", "d1/.f2", "d1/f1", "f1"]
328+
)
316329

317330
# with ignore
318331
filepaths = included_paths(self.path_to("root"), match=["*", "!.*/"])
319-
assert filepaths == [".f2", "d1/.f2", "d1/f1", "f1"]
332+
assert filepaths == map_osp([".f2", "d1/.f2", "d1/f1", "f1"])
320333

321334
def test_exclude_hidden_dirs_and_files(self):
322335
self.mkdirs("root/d1")
@@ -330,11 +343,11 @@ def test_exclude_hidden_dirs_and_files(self):
330343

331344
# no ignore
332345
filepaths = included_paths(self.path_to("root"))
333-
assert filepaths == [".d2/f1", ".f2", "d1/.f2", "d1/f1", "f1"]
346+
assert filepaths == map_osp([".d2/f1", ".f2", "d1/.f2", "d1/f1", "f1"])
334347

335348
# using ignore
336349
filepaths = included_paths(self.path_to("root"), match=["*", "!.*/", "!.*"])
337-
assert filepaths == ["d1/f1", "f1"]
350+
assert filepaths == map_osp(["d1/f1", "f1"])
338351

339352
def test_exclude_extensions(self):
340353
self.mkdirs("root/d1")
@@ -353,14 +366,16 @@ def test_exclude_extensions(self):
353366
filepaths = included_paths(
354367
self.path_to("root"), match=["*", "!*.skip1", "!*.skip2"]
355368
)
356-
assert filepaths == [
357-
"d1/f.txt",
358-
"f",
359-
"f.skip1.txt",
360-
"f.skip1skip2",
361-
"f.txt",
362-
"fskip1",
363-
]
369+
assert filepaths == map_osp(
370+
[
371+
"d1/f.txt",
372+
"f",
373+
"f.skip1.txt",
374+
"f.skip1skip2",
375+
"f.txt",
376+
"fskip1",
377+
]
378+
)
364379

365380
def test_empty_dirs_include_vs_exclude(self):
366381
self.mkdirs("root/d1")
@@ -372,14 +387,14 @@ def test_empty_dirs_include_vs_exclude(self):
372387
self.mkfile("root/d3/d31/f")
373388

374389
filepaths = included_paths(self.path_to("root"), empty_dirs=False)
375-
assert filepaths == ["d1/f", "d3/d31/f"]
390+
assert filepaths == map_osp(["d1/f", "d3/d31/f"])
376391

377392
# `include_empty=False` is default
378393
filepaths = included_paths(self.path_to("root"))
379-
assert filepaths == ["d1/f", "d3/d31/f"]
394+
assert filepaths == map_osp(["d1/f", "d3/d31/f"])
380395

381396
filepaths = included_paths(self.path_to("root"), empty_dirs=True)
382-
assert filepaths == ["d1/f", "d2/.", "d3/d31/f", "d4/d41/."]
397+
assert filepaths == map_osp(["d1/f", "d2/.", "d3/d31/f", "d4/d41/."])
383398

384399
def test_empty_dirs_because_of_filter_include_vs_exclude(self):
385400
self.mkdirs("root/d1")
@@ -391,19 +406,19 @@ def test_empty_dirs_because_of_filter_include_vs_exclude(self):
391406
filepaths = included_paths(
392407
self.path_to("root"), match=["*", "!.*"], empty_dirs=False
393408
)
394-
assert filepaths == ["d1/f"]
409+
assert filepaths == map_osp(["d1/f"])
395410

396411
# `include_empty=False` is default
397412
filepaths = included_paths(
398413
self.path_to("root"),
399414
match=["*", "!.*"],
400415
)
401-
assert filepaths == ["d1/f"]
416+
assert filepaths == map_osp(["d1/f"])
402417

403418
filepaths = included_paths(
404419
self.path_to("root"), match=["*", "!.*"], empty_dirs=True
405420
)
406-
assert filepaths == ["d1/f", "d2/."]
421+
assert filepaths == map_osp(["d1/f", "d2/."])
407422

408423
def test_empty_dir_inclusion_not_affected_by_match(self):
409424
self.mkdirs("root/d1")
@@ -414,17 +429,17 @@ def test_empty_dir_inclusion_not_affected_by_match(self):
414429
filepaths = included_paths(
415430
self.path_to("root"), match=["*", "!.*"], empty_dirs=True
416431
)
417-
assert filepaths == [".d2/.", "d1/."]
432+
assert filepaths == map_osp([".d2/.", "d1/."])
418433

419434
filepaths = included_paths(
420435
self.path_to("root"), match=["*", "!.*/"], empty_dirs=True
421436
)
422-
assert filepaths == [".d2/.", "d1/."]
437+
assert filepaths == map_osp([".d2/.", "d1/."])
423438

424439
filepaths = included_paths(
425440
self.path_to("root"), match=["*", "!d1"], empty_dirs=True
426441
)
427-
assert filepaths == [".d2/.", "d1/."]
442+
assert filepaths == map_osp([".d2/.", "d1/."])
428443

429444

430445
def dirhash_mp_comp(*args, **kwargs):
@@ -658,6 +673,11 @@ def test_raise_on_not_at_least_one_of_name_and_data(self):
658673
self.path_to("root1"), "sha256", entry_properties=["is_link"]
659674
)
660675

676+
@pytest.mark.skipif(
677+
os.name == "nt",
678+
reason="TODO: not getting expected speedup on Windows.",
679+
# TODO: see https://github.com/andhus/scantree/issues/25
680+
)
661681
def test_multiproc_speedup(self):
662682
self.mkdirs("root/dir")
663683
num_files = 10
@@ -704,7 +724,7 @@ def test_cache_by_real_path_speedup(self, tmpdir):
704724
target_file = tmpdir.join("target_file")
705725
target_file.ensure()
706726
for i in range(num_links):
707-
root2.join(f"link_{i}").mksymlinkto(target_file)
727+
os.symlink(target_file, root2.join(f"link_{i}"))
708728

709729
overhead_margin_factor = 1.5
710730
expected_max_elapsed_with_links = overhead * overhead_margin_factor + wait_time
@@ -743,7 +763,7 @@ def test_cache_together_with_multiprocess_speedup(self, tmpdir):
743763
target_file = tmpdir.join(target_file_name)
744764
target_file.write("< one chunk content", ensure=True)
745765
for j in range(num_links_per_file):
746-
root2.join(f"link_{i}_{j}").mksymlinkto(target_file)
766+
os.symlink(target_file, root2.join(f"link_{i}_{j}"))
747767

748768
overhead_margin_factor = 1.5
749769
expected_max_elapsed_with_links = (

0 commit comments

Comments
 (0)