Skip to content

Commit 1aa845d

Browse files
authored
Re-work colcon_core.command.get_prog_name (#617)
This function's purpose is to handle these special cases of argv[0]: * Invoked using python -m ... * Invoked using a path to the executable even though the executable is on the PATH This change enhances the path comparison to support normalization of that path, Windows long path prefixes, and also the easy-install behavior on Windows where argv[0] has no extension. Yet to be properly handled is invocation using python -c ...
1 parent 857ea3f commit 1aa845d

File tree

3 files changed

+109
-3
lines changed

3 files changed

+109
-3
lines changed

colcon_core/command.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,28 @@ def get_prog_name():
275275
if basename == '__main__.py':
276276
# use the module name in case the script was invoked with python -m ...
277277
prog = os.path.basename(os.path.dirname(prog))
278-
elif shutil.which(basename) == prog:
279-
# use basename only if it is on the PATH
280-
prog = basename
278+
else:
279+
default_prog = shutil.which(basename) or ''
280+
default_ext = os.path.splitext(default_prog)[1]
281+
real_prog = prog
282+
if (
283+
sys.platform == 'win32' and
284+
os.path.splitext(real_prog)[1] != default_ext
285+
):
286+
# On Windows, setuptools entry points drop the file extension from
287+
# argv[0], but shutil.which does not. If the two don't end in the
288+
# same extension, try appending the shutil extension for a better
289+
# chance at matching.
290+
real_prog += default_ext
291+
try:
292+
# The os.path.samefile requires that both files exist on disk, but
293+
# has the advantage of working around symlinks, UNC-style paths,
294+
# DOS 8.3 path atoms, and path normalization.
295+
if os.path.samefile(default_prog, real_prog):
296+
# use basename only if it is on the PATH
297+
prog = basename
298+
except (FileNotFoundError, NotADirectoryError):
299+
pass
281300
return prog
282301

283302

test/spell_check.words

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ setuptools
116116
shlex
117117
sigint
118118
sitecustomize
119+
skipif
119120
sloretz
120121
stacklevel
121122
staticmethod

test/test_command.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
# Copyright 2016-2018 Dirk Thomas
22
# Licensed under the Apache License, Version 2.0
33

4+
import os
45
import shutil
56
import signal
67
import sys
78
from tempfile import mkdtemp
9+
from tempfile import TemporaryDirectory
810
from unittest.mock import Mock
911
from unittest.mock import patch
1012

1113
from colcon_core.command import CommandContext
1214
from colcon_core.command import create_parser
15+
from colcon_core.command import get_prog_name
1316
from colcon_core.command import main
1417
from colcon_core.command import verb_main
1518
from colcon_core.environment_variable import EnvironmentVariable
@@ -151,3 +154,86 @@ def test_verb_main():
151154
assert logger.error.call_args[0][0].startswith(
152155
'command_name verb_name: custom error message\n')
153156
assert 'Exception: custom error message' in logger.error.call_args[0][0]
157+
158+
159+
def test_prog_name_module():
160+
argv = [os.path.join('foo', 'bar', '__main__.py')]
161+
with patch('colcon_core.command.sys.argv', argv):
162+
# prog should be the module containing __main__.py
163+
assert get_prog_name() == 'bar'
164+
165+
166+
def test_prog_name_on_path():
167+
# use __file__ since we know it exists
168+
argv = [__file__]
169+
with patch('colcon_core.command.sys.argv', argv):
170+
with patch(
171+
'colcon_core.command.shutil.which',
172+
return_value=__file__
173+
):
174+
# prog should be shortened to the basename
175+
assert get_prog_name() == 'test_command.py'
176+
177+
178+
def test_prog_name_not_on_path():
179+
# use __file__ since we know it exists
180+
argv = [__file__]
181+
with patch('colcon_core.command.sys.argv', argv):
182+
with patch('colcon_core.command.shutil.which', return_value=None):
183+
# prog should remain unchanged
184+
assert get_prog_name() == __file__
185+
186+
187+
def test_prog_name_different_on_path():
188+
# use __file__ since we know it exists
189+
argv = [__file__]
190+
with patch('colcon_core.command.sys.argv', argv):
191+
with patch(
192+
'colcon_core.command.shutil.which',
193+
return_value=sys.executable
194+
):
195+
# prog should remain unchanged
196+
assert get_prog_name() == __file__
197+
198+
199+
def test_prog_name_not_a_file():
200+
# pick some file that doesn't actually exist on disk
201+
no_such_file = os.path.join(__file__, 'foobar')
202+
argv = [no_such_file]
203+
with patch('colcon_core.command.sys.argv', argv):
204+
with patch(
205+
'colcon_core.command.shutil.which',
206+
return_value=no_such_file
207+
):
208+
# prog should remain unchanged
209+
assert get_prog_name() == no_such_file
210+
211+
212+
@pytest.mark.skipif(sys.platform == 'win32', reason='Symlinks not supported.')
213+
def test_prog_name_symlink():
214+
# use __file__ since we know it exists
215+
with TemporaryDirectory(prefix='test_colcon_') as temp_dir:
216+
linked_file = os.path.join(temp_dir, 'test_command.py')
217+
os.symlink(__file__, linked_file)
218+
219+
argv = [linked_file]
220+
with patch('colcon_core.command.sys.argv', argv):
221+
with patch(
222+
'colcon_core.command.shutil.which',
223+
return_value=__file__
224+
):
225+
# prog should be shortened to the basename
226+
assert get_prog_name() == 'test_command.py'
227+
228+
229+
@pytest.mark.skipif(sys.platform != 'win32', reason='Only valid on Windows.')
230+
def test_prog_name_easy_install():
231+
# use __file__ since we know it exists
232+
argv = [__file__[:-3]]
233+
with patch('colcon_core.command.sys.argv', argv):
234+
with patch(
235+
'colcon_core.command.shutil.which',
236+
return_value=__file__
237+
):
238+
# prog should be shortened to the basename
239+
assert get_prog_name() == 'test_command'

0 commit comments

Comments
 (0)