Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 82a9481

Browse files
authored
bpo-33185: Fix regression in pydoc CLI sys.path handling (GH-6419)
The pydoc CLI assumed -m pydoc would add the empty string to sys.path, and hence got confused when it switched to adding the full initial working directory instead. This refactors the pydoc CLI path manipulation to be more testable, and ensures it won't accidentally remove the standard library directory containing pydoc itself from sys.path.
1 parent 480ab05 commit 82a9481

4 files changed

Lines changed: 115 additions & 10 deletions

File tree

Doc/whatsnew/3.7.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,9 +1145,10 @@ Changes in Python behavior
11451145
(Contributed by Serhiy Storchaka in :issue:`32012` and :issue:`32023`.)
11461146

11471147
* When using the ``-m`` switch, the starting directory is now added to sys.path,
1148-
rather than the current working directory. Any programs that are found to be
1149-
relying on the previous behaviour will need to be updated to manipulate
1150-
:data:`sys.path` appropriately.
1148+
rather than the current working directory. Any programs that are checking for
1149+
the empty string in :data:`sys.path`, or otherwise relying on the previous
1150+
behaviour, will need to be updated accordingly (e.g. by checking for
1151+
``os.getcwd()`` in addition to checking for the empty string).
11511152

11521153

11531154
Changes in the Python API

Lib/pydoc.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2614,18 +2614,50 @@ def browse(port=0, *, open_browser=True, hostname='localhost'):
26142614
def ispath(x):
26152615
return isinstance(x, str) and x.find(os.sep) >= 0
26162616

2617+
def _get_revised_path(given_path, argv0):
2618+
"""Ensures current directory is on returned path, and argv0 directory is not
2619+
2620+
Exception: argv0 dir is left alone if it's also pydoc's directory.
2621+
2622+
Returns a new path entry list, or None if no adjustment is needed.
2623+
"""
2624+
# Scripts may get the current directory in their path by default if they're
2625+
# run with the -m switch, or directly from the current directory.
2626+
# The interactive prompt also allows imports from the current directory.
2627+
2628+
# Accordingly, if the current directory is already present, don't make
2629+
# any changes to the given_path
2630+
if '' in given_path or os.curdir in given_path or os.getcwd() in given_path:
2631+
return None
2632+
2633+
# Otherwise, add the current directory to the given path, and remove the
2634+
# script directory (as long as the latter isn't also pydoc's directory.
2635+
stdlib_dir = os.path.dirname(__file__)
2636+
script_dir = os.path.dirname(argv0)
2637+
revised_path = given_path.copy()
2638+
if script_dir in given_path and not os.path.samefile(script_dir, stdlib_dir):
2639+
revised_path.remove(script_dir)
2640+
revised_path.insert(0, os.getcwd())
2641+
return revised_path
2642+
2643+
2644+
# Note: the tests only cover _get_revised_path, not _adjust_cli_path itself
2645+
def _adjust_cli_sys_path():
2646+
"""Ensures current directory is on sys.path, and __main__ directory is not
2647+
2648+
Exception: __main__ dir is left alone if it's also pydoc's directory.
2649+
"""
2650+
revised_path = _get_revised_path(sys.path, sys.argv[0])
2651+
if revised_path is not None:
2652+
sys.path[:] = revised_path
2653+
2654+
26172655
def cli():
26182656
"""Command-line interface (looks at sys.argv to decide what to do)."""
26192657
import getopt
26202658
class BadUsage(Exception): pass
26212659

2622-
# Scripts don't get the current directory in their path by default
2623-
# unless they are run with the '-m' switch
2624-
if '' not in sys.path:
2625-
scriptdir = os.path.dirname(sys.argv[0])
2626-
if scriptdir in sys.path:
2627-
sys.path.remove(scriptdir)
2628-
sys.path.insert(0, '.')
2660+
_adjust_cli_sys_path()
26292661

26302662
try:
26312663
opts, args = getopt.getopt(sys.argv[1:], 'bk:n:p:w')

Lib/test/test_pydoc.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import re
1212
import stat
1313
import string
14+
import tempfile
1415
import test.support
1516
import time
1617
import types
@@ -1084,6 +1085,71 @@ def test_resolve_false(self):
10841085
self.assertIn('class Enum', helptext)
10851086

10861087

1088+
class TestInternalUtilities(unittest.TestCase):
1089+
1090+
def setUp(self):
1091+
tmpdir = tempfile.TemporaryDirectory()
1092+
self.argv0dir = tmpdir.name
1093+
self.argv0 = os.path.join(tmpdir.name, "nonexistent")
1094+
self.addCleanup(tmpdir.cleanup)
1095+
self.abs_curdir = abs_curdir = os.getcwd()
1096+
self.curdir_spellings = ["", os.curdir, abs_curdir]
1097+
1098+
def _get_revised_path(self, given_path, argv0=None):
1099+
# Checking that pydoc.cli() actually calls pydoc._get_revised_path()
1100+
# is handled via code review (at least for now).
1101+
if argv0 is None:
1102+
argv0 = self.argv0
1103+
return pydoc._get_revised_path(given_path, argv0)
1104+
1105+
def _get_starting_path(self):
1106+
# Get a copy of sys.path without the current directory
1107+
clean_path = sys.path.copy()
1108+
for spelling in self.curdir_spellings:
1109+
for __ in range(clean_path.count(spelling)):
1110+
clean_path.remove(spelling)
1111+
return clean_path
1112+
1113+
def test_sys_path_adjustment_adds_missing_curdir(self):
1114+
clean_path = self._get_starting_path()
1115+
expected_path = [self.abs_curdir] + clean_path
1116+
self.assertEqual(self._get_revised_path(clean_path), expected_path)
1117+
1118+
def test_sys_path_adjustment_removes_argv0_dir(self):
1119+
clean_path = self._get_starting_path()
1120+
expected_path = [self.abs_curdir] + clean_path
1121+
leading_argv0dir = [self.argv0dir] + clean_path
1122+
self.assertEqual(self._get_revised_path(leading_argv0dir), expected_path)
1123+
trailing_argv0dir = clean_path + [self.argv0dir]
1124+
self.assertEqual(self._get_revised_path(trailing_argv0dir), expected_path)
1125+
1126+
1127+
def test_sys_path_adjustment_protects_pydoc_dir(self):
1128+
def _get_revised_path(given_path):
1129+
return self._get_revised_path(given_path, argv0=pydoc.__file__)
1130+
clean_path = self._get_starting_path()
1131+
leading_argv0dir = [self.argv0dir] + clean_path
1132+
expected_path = [self.abs_curdir] + leading_argv0dir
1133+
self.assertEqual(_get_revised_path(leading_argv0dir), expected_path)
1134+
trailing_argv0dir = clean_path + [self.argv0dir]
1135+
expected_path = [self.abs_curdir] + trailing_argv0dir
1136+
self.assertEqual(_get_revised_path(trailing_argv0dir), expected_path)
1137+
1138+
def test_sys_path_adjustment_when_curdir_already_included(self):
1139+
clean_path = self._get_starting_path()
1140+
for spelling in self.curdir_spellings:
1141+
with self.subTest(curdir_spelling=spelling):
1142+
# If curdir is already present, no alterations are made at all
1143+
leading_curdir = [spelling] + clean_path
1144+
self.assertIsNone(self._get_revised_path(leading_curdir))
1145+
trailing_curdir = clean_path + [spelling]
1146+
self.assertIsNone(self._get_revised_path(trailing_curdir))
1147+
leading_argv0dir = [self.argv0dir] + leading_curdir
1148+
self.assertIsNone(self._get_revised_path(leading_argv0dir))
1149+
trailing_argv0dir = trailing_curdir + [self.argv0dir]
1150+
self.assertIsNone(self._get_revised_path(trailing_argv0dir))
1151+
1152+
10871153
@reap_threads
10881154
def test_main():
10891155
try:
@@ -1094,6 +1160,7 @@ def test_main():
10941160
PydocUrlHandlerTest,
10951161
TestHelper,
10961162
PydocWithMetaClasses,
1163+
TestInternalUtilities,
10971164
)
10981165
finally:
10991166
reap_children()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed regression when running pydoc with the ``-m`` switch. (The regression
2+
was introduced in 3.7.0b3 by the resolution of bpo-33053)
3+
4+
This fix also changed pydoc to add ``os.getcwd()`` to ``sys.path`` when
5+
necessary, rather than adding ``"."``.

0 commit comments

Comments
 (0)