Skip to content

Commit a79aef3

Browse files
authored
Allow spaces (and any other characters that work on the platform) in paths (#327)
* Remove validate_path and use a path with space in tests (set up to fail) * Change test to space in path + special character; use bat rather than ps1 on win b/c easier to quote properly * Reformat with black
1 parent cf6b016 commit a79aef3

File tree

8 files changed

+56
-64
lines changed

8 files changed

+56
-64
lines changed

.gitignore

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,7 @@ dmypy.json
135135
.vscode/
136136

137137
# test files
138-
tests/tmp
139-
tests/videos
140-
tests/ground_truths*
138+
tests/test data
141139

142140
# VSCode
143-
.vscode/
141+
.vscode/

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ dependencies:
99
- tqdm
1010
- psutil
1111
- filelock >= 3.15.1
12+
- mslex

environment_rtd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ dependencies:
99
- psutil
1010
- pydata-sphinx-theme < 0.10.0
1111
- filelock >= 3.15.1
12+
- mslex

mesmerize_core/batch_utils.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
import pandas as pd
77

8-
from .utils import validate_path
9-
108
CURRENT_BATCH_PATH: Path = None # only one batch at a time
119
PARENT_DATA_PATH: Path = None
1210

@@ -44,7 +42,7 @@ def set_parent_raw_data_path(path: Union[Path, str]) -> Path:
4442
Full parent data path
4543
"""
4644
global PARENT_DATA_PATH
47-
path = Path(validate_path(path))
45+
path = Path(path)
4846
if not path.is_dir():
4947
raise NotADirectoryError(
5048
"The directory passed to `set_parent_raw_data_path()` does not exist.\n"
@@ -192,8 +190,6 @@ def load_batch(path: Union[str, Path]) -> pd.DataFrame:
192190
193191
"""
194192

195-
path = validate_path(path)
196-
197193
df = pd.read_pickle(Path(path))
198194

199195
df.paths.set_batch_path(path)
@@ -235,8 +231,6 @@ def create_batch(path: Union[str, Path], remove_existing: bool = False) -> pd.Da
235231
df = create_batch("/path/to/new_batch.pickle")
236232
237233
"""
238-
path = validate_path(path)
239-
240234
if Path(path).is_file():
241235
if remove_existing:
242236
os.remove(path)

mesmerize_core/caiman_extensions/common.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from datetime import datetime
1010
import time
1111
from copy import deepcopy
12-
import shlex
1312

1413
import numpy as np
1514
import pandas as pd
@@ -29,11 +28,16 @@
2928
get_parent_raw_data_path,
3029
load_batch,
3130
)
32-
from ..utils import validate_path, IS_WINDOWS, make_runfile, warning_experimental
31+
from ..utils import IS_WINDOWS, make_runfile, warning_experimental
3332
from .cnmf import cnmf_cache
3433
from .. import algorithms
3534
from ..movie_readers import default_reader
3635

36+
import shlex
37+
import mslex
38+
39+
lex = mslex if IS_WINDOWS else shlex
40+
3741

3842
ALGO_MODULES = {
3943
"cnmf": algorithms.cnmf,
@@ -113,7 +117,6 @@ def add_item(
113117

114118
# make sure path is within batch dir or parent raw data path
115119
input_movie_path = self._df.paths.resolve(input_movie_path)
116-
validate_path(input_movie_path)
117120

118121
# get relative path
119122
input_movie_path = self._df.paths.split(input_movie_path)[1]
@@ -545,10 +548,7 @@ def _run_subprocess(self, runfile_path: str, wait: bool, **kwargs):
545548

546549
# Get the dir that contains the input movie
547550
parent_path = self._series.paths.resolve(self._series.input_movie_path).parent
548-
if not IS_WINDOWS:
549-
self.process = Popen(runfile_path, cwd=parent_path)
550-
else:
551-
self.process = Popen(f"powershell {runfile_path}", cwd=parent_path)
551+
self.process = Popen([runfile_path], cwd=parent_path)
552552

553553
if wait:
554554
self.process.wait()
@@ -647,16 +647,18 @@ def run(self, backend: Optional[str] = None, wait: bool = True, **kwargs):
647647

648648
# Create the runfile in the batch dir using this Series' UUID as the filename
649649
if IS_WINDOWS:
650-
runfile_ext = ".ps1"
650+
runfile_ext = ".bat"
651651
else:
652652
runfile_ext = ".runfile"
653653
runfile_path = str(
654654
batch_path.parent.joinpath(self._series["uuid"] + runfile_ext)
655655
)
656656

657-
args_str = f"--batch-path {batch_path} --uuid {self._series.uuid}"
657+
args_str = (
658+
f"--batch-path {lex.quote(str(batch_path))} --uuid {self._series.uuid}"
659+
)
658660
if get_parent_raw_data_path() is not None:
659-
args_str += f" --data-path {get_parent_raw_data_path()}"
661+
args_str += f" --data-path {lex.quote(str(get_parent_raw_data_path()))}"
660662

661663
# make the runfile
662664
runfile_path = make_runfile(
@@ -666,14 +668,10 @@ def run(self, backend: Optional[str] = None, wait: bool = True, **kwargs):
666668
filename=runfile_path, # path to create runfile
667669
args_str=args_str,
668670
)
669-
try:
670-
self.process = getattr(self, f"_run_{backend}")(
671-
runfile_path, wait=wait, **kwargs
672-
)
673-
except:
674-
with open(runfile_path, "r") as f:
675-
raise ValueError(f.read())
676671

672+
self.process = getattr(self, f"_run_{backend}")(
673+
runfile_path, wait=wait, **kwargs
674+
)
677675
return self.process
678676

679677
def get_input_movie_path(self) -> Path:

mesmerize_core/utils.py

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@
1515
import sys
1616
from tempfile import NamedTemporaryFile
1717
from subprocess import check_call
18+
import shlex
19+
import mslex
1820

1921
if os.name == "nt":
2022
IS_WINDOWS = True
2123
HOME = "USERPROFILE"
24+
lex = mslex
2225
else:
2326
IS_WINDOWS = False
2427
HOME = "HOME"
28+
lex = shlex
2529

2630
if "MESMERIZE_LRU_CACHE" in os.environ.keys():
2731
MESMERIZE_LRU_CACHE = os.environ["MESMERIZE_LRU_CACHE"]
@@ -52,15 +56,6 @@ def fn(self, *args, **kwargs):
5256
return catcher
5357

5458

55-
def validate_path(path: Union[str, Path]):
56-
if not regex.match(r"^[A-Za-z0-9@/\\:._-]*$", str(path)):
57-
raise ValueError(
58-
"Paths must only contain alphanumeric characters, "
59-
"hyphens ( - ), underscores ( _ ) or periods ( . )"
60-
)
61-
return path
62-
63-
6459
def make_runfile(
6560
module_path: str, args_str: Optional[str] = None, filename: Optional[str] = None
6661
) -> str:
@@ -87,13 +82,13 @@ def make_runfile(
8782

8883
if filename is None:
8984
if IS_WINDOWS:
90-
sh_file = os.path.join(os.environ[HOME], "run.ps1")
85+
filename = os.path.join(os.environ[HOME], "run.bat")
9186
else:
92-
sh_file = os.path.join(os.environ[HOME], "run.sh")
87+
filename = os.path.join(os.environ[HOME], "run.sh")
9388
else:
9489
if IS_WINDOWS:
95-
if not filename.endswith(".ps1"):
96-
filename = filename + ".ps1"
90+
if not filename.endswith(".bat"):
91+
filename = filename + ".bat"
9792

9893
sh_file = filename
9994

@@ -107,13 +102,13 @@ def make_runfile(
107102

108103
if "VIRTUAL_ENV" in os.environ.keys():
109104
f.write(
110-
f'export PATH={os.environ["PATH"]}\n'
111-
f'export VIRTUAL_ENV={os.environ["VIRTUAL_ENV"]}\n'
112-
f'export LD_LIBRARY_PATH={os.environ["LD_LIBRARY_PATH"]}\n'
105+
f'export PATH={lex.quote(os.environ["PATH"])}\n'
106+
f'export VIRTUAL_ENV={lex.quote(os.environ["VIRTUAL_ENV"])}\n'
107+
f'export LD_LIBRARY_PATH={lex.quote(os.environ["LD_LIBRARY_PATH"])}\n'
113108
)
114109

115110
if "PYTHONPATH" in os.environ.keys():
116-
f.write(f'export PYTHONPATH={os.environ["PYTHONPATH"]}\n')
111+
f.write(f'export PYTHONPATH={lex.quote(os.environ["PYTHONPATH"])}\n')
117112

118113
# for k, v in os.environ.items(): # copy the current environment
119114
# if '\n' in v:
@@ -133,28 +128,29 @@ def make_runfile(
133128
# add command to run the python script in the conda environment
134129
# that was active at the time that this shell script was generated
135130
f.write(
136-
f'{os.environ["CONDA_EXE"]} run -p {os.environ["CONDA_PREFIX"]} python {module_path} {args_str}'
131+
f'{lex.quote(os.environ["CONDA_EXE"])} run -p {lex.quote(os.environ["CONDA_PREFIX"])} '
132+
f"python {lex.quote(module_path)} {args_str}"
137133
)
138134
else:
139-
f.write(f"python {module_path} {args_str}") # call the script to run
135+
f.write(
136+
f"python {lex.quote(module_path)} {args_str}"
137+
) # call the script to run
140138

141139
else:
142140
with open(sh_file, "w") as f:
143141
for k, v in os.environ.items(): # copy the current environment
144142
if regex.match(r"^.*[()]", str(k)) or regex.match(r"^.*[()]", str(v)):
145143
continue
146-
with NamedTemporaryFile(suffix=".ps1", delete=False) as tmp:
147-
try: # windows powershell is stupid so make sure all the env var names work
148-
tmp.write(f'$env:{k}="{v}";\n')
149-
tmp.close()
150-
check_call(f"powershell {tmp.name}")
151-
os.unlink(tmp.name)
152-
except:
153-
continue
154-
f.write(
155-
f'$env:{k}="{v}";\n'
156-
) # write only env vars that powershell likes
157-
f.write(f"{sys.executable} {module_path} {args_str}")
144+
# with NamedTemporaryFile(suffix=".bat", delete=False, mode="w") as tmp:
145+
# try: # windows powershell is stupid so make sure all the env var names work
146+
# tmp.write(f'$env:{k}="{v}";\n')
147+
# tmp.close()
148+
# check_call(f"powershell {tmp.name}")
149+
# os.unlink(tmp.name)
150+
# except:
151+
# continue
152+
f.write(f"SET {k}={lex.quote(v)};\n")
153+
f.write(f"{lex.quote(sys.executable)} {lex.quote(module_path)} {args_str}")
158154

159155
st = os.stat(sh_file)
160156
os.chmod(sh_file, st.st_mode | S_IEXEC)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ setuptools
66
wheel
77
numpy
88
matplotlib
9+
mslex
910
click
1011
psutil
1112
jupyterlab

tests/test_core.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
from copy import deepcopy
3535

3636
# don't call "resolve" on these - want to make sure we can handle non-canonical paths correctly
37-
tmp_dir = Path(os.path.dirname(os.path.abspath(__file__)), "tmp")
38-
vid_dir = Path(os.path.dirname(os.path.abspath(__file__)), "videos")
39-
ground_truths_dir = Path(os.path.dirname(os.path.abspath(__file__)), "ground_truths")
37+
tmp_dir = Path(os.path.dirname(os.path.abspath(__file__)), "test data", "tmp")
38+
vid_dir = Path(os.path.dirname(os.path.abspath(__file__)), "test data", "videos")
39+
ground_truths_dir = Path(
40+
os.path.dirname(os.path.abspath(__file__)), "test data", "ground_truths"
41+
)
4042
ground_truths_file = Path(
41-
os.path.dirname(os.path.abspath(__file__)), "ground_truths.zip"
43+
os.path.dirname(os.path.abspath(__file__)), "test data", "ground_truths.zip"
4244
)
4345

4446
os.makedirs(tmp_dir, exist_ok=True)
@@ -74,7 +76,8 @@ def _download_ground_truths():
7476

7577

7678
def get_tmp_filename():
77-
return os.path.join(tmp_dir, f"{uuid4()}.pickle")
79+
# add a $ (legal on both UNIX and Windows) to ensure we are escaping it correctly
80+
return os.path.join(tmp_dir, f"{uuid4()}$test.pickle")
7881

7982

8083
def clear_tmp():

0 commit comments

Comments
 (0)