Implemented PEP 370

This commit is contained in:
Christian Heimes 2008-05-06 22:41:46 +00:00
parent 323c40d48c
commit af748c3ab8
10 changed files with 397 additions and 62 deletions

View File

@ -85,3 +85,51 @@ Note that for some non-Unix systems, ``sys.prefix`` and ``sys.exec_prefix`` are
empty, and the path manipulations are skipped; however the import of
:mod:`sitecustomize` is still attempted.
.. data:: PREFIXES
A list of prefixes for site package directories
.. versionadded:: 2.6
.. data:: ENABLE_USER_SITE
Flag showing the status of the user site directory. True means the
user site directory is enabled and added to sys.path. When the flag
is None the user site directory is disabled for security reasons.
.. versionadded:: 2.6
.. data:: USER_SITE
Path to the user site directory for the current Python version or None
.. versionadded:: 2.6
.. data:: USER_BASE
Path to the base directory for user site directories
.. versionadded:: 2.6
.. envvar:: PYTHONNOUSERSITE
.. versionadded:: 2.6
.. envvar:: PYTHONUSERBASE
.. versionadded:: 2.6
.. function:: addsitedir(sitedir, known_paths=None)
Adds a directory to sys.path and processes its pth files.
XXX Update documentation
XXX document python -m site --user-base --user-site

View File

@ -21,7 +21,7 @@ Command line
When invoking Python, you may specify any of these options::
python [-dEiOQStuUvxX3?] [-c command | -m module-name | script | - ] [args]
python [-dEiOQsStuUvxX3?] [-c command | -m module-name | script | - ] [args]
The most common use case is, of course, a simple invocation of a script::
@ -240,6 +240,17 @@ Miscellaneous options
:pep:`238` -- Changing the division operator
.. cmdoption:: -s
Don't add user site directory to sys.path
.. versionadded:: 2.6
.. seealso::
:pep:`370` -- Per user site-packages directory
.. cmdoption:: -S
Disable the import of the module :mod:`site` and the site-dependent
@ -471,6 +482,28 @@ These environment variables influence Python's behavior.
.. versionadded:: 2.6
.. envvar:: PYTHONNOUSERSITE
If this is set, Python won't add the user site directory to sys.path
.. versionadded:: 2.6
.. seealso::
:pep:`370` -- Per user site-packages directory
.. envvar:: PYTHONUSERBASE
Sets the base directory for the user site directory
.. versionadded:: 2.6
.. seealso::
:pep:`370` -- Per user site-packages directory
.. envvar:: PYTHONEXECUTABLE
If this environment variable is set, ``sys.argv[0]`` will be set to its

View File

@ -19,6 +19,7 @@ PyAPI_DATA(int) Py_UnicodeFlag;
PyAPI_DATA(int) Py_IgnoreEnvironmentFlag;
PyAPI_DATA(int) Py_DivisionWarningFlag;
PyAPI_DATA(int) Py_DontWriteBytecodeFlag;
PyAPI_DATA(int) Py_NoUserSiteDirectory;
/* _XXX Py_QnewFlag should go away in 3.0. It's true iff -Qnew is passed,
on the command line, and is used in 2.2 by ceval.c to make all "/" divisions
true divisions (which they will be in 3.0). */

View File

@ -18,6 +18,9 @@ from distutils.file_util import write_file
from distutils.util import convert_path, subst_vars, change_root
from distutils.util import get_platform
from distutils.errors import DistutilsOptionError
from site import USER_BASE
from site import USER_SITE
if sys.version < "2.2":
WINDOWS_SCHEME = {
@ -51,7 +54,21 @@ INSTALL_SCHEMES = {
'scripts': '$base/bin',
'data' : '$base',
},
'unix_user': {
'purelib': '$usersite',
'platlib': '$usersite',
'headers': '$userbase/include/python$py_version_short/$dist_name',
'scripts': '$userbase/bin',
'data' : '$userbase',
},
'nt': WINDOWS_SCHEME,
'nt_user': {
'purelib': '$usersite',
'platlib': '$usersite',
'headers': '$userbase/Python$py_version_nodot/Include/$dist_name',
'scripts': '$userbase/Scripts',
'data' : '$userbase',
},
'mac': {
'purelib': '$base/Lib/site-packages',
'platlib': '$base/Lib/site-packages',
@ -59,13 +76,27 @@ INSTALL_SCHEMES = {
'scripts': '$base/Scripts',
'data' : '$base',
},
'mac_user': {
'purelib': '$usersite',
'platlib': '$usersite',
'headers': '$userbase/$py_version_short/include/$dist_name',
'scripts': '$userbase/bin',
'data' : '$userbase',
},
'os2': {
'purelib': '$base/Lib/site-packages',
'platlib': '$base/Lib/site-packages',
'headers': '$base/Include/$dist_name',
'scripts': '$base/Scripts',
'data' : '$base',
}
},
'os2_home': {
'purelib': '$usersite',
'platlib': '$usersite',
'headers': '$userbase/include/python$py_version_short/$dist_name',
'scripts': '$userbase/bin',
'data' : '$userbase',
},
}
# The keys to an installation scheme; if any new types of files are to be
@ -86,6 +117,8 @@ class install (Command):
"(Unix only) prefix for platform-specific files"),
('home=', None,
"(Unix only) home directory to install under"),
('user', None,
"install in user site-package '%s'" % USER_SITE),
# Or, just set the base director(y|ies)
('install-base=', None,
@ -137,7 +170,7 @@ class install (Command):
"filename in which to record list of installed files"),
]
boolean_options = ['compile', 'force', 'skip-build']
boolean_options = ['compile', 'force', 'skip-build', 'user']
negative_opt = {'no-compile' : 'compile'}
@ -148,6 +181,7 @@ class install (Command):
self.prefix = None
self.exec_prefix = None
self.home = None
self.user = 0
# These select only the installation base; it's up to the user to
# specify the installation scheme (currently, that means supplying
@ -166,6 +200,8 @@ class install (Command):
self.install_lib = None # set to either purelib or platlib
self.install_scripts = None
self.install_data = None
self.install_userbase = USER_BASE
self.install_usersite = USER_SITE
self.compile = None
self.optimize = None
@ -241,6 +277,11 @@ class install (Command):
raise DistutilsOptionError, \
"must supply either home or prefix/exec-prefix -- not both"
if self.user and (self.prefix or self.exec_prefix or self.home or
self.install_base or self.install_platbase):
raise DistutilsOptionError("can't combine user with with prefix/"
"exec_prefix/home or install_(plat)base")
# Next, stuff that's wrong (or dubious) only on certain platforms.
if os.name != "posix":
if self.exec_prefix:
@ -276,10 +317,13 @@ class install (Command):
'dist_fullname': self.distribution.get_fullname(),
'py_version': py_version,
'py_version_short': py_version[0:3],
'py_version_nodot': py_version[0] + py_version[2],
'sys_prefix': prefix,
'prefix': prefix,
'sys_exec_prefix': exec_prefix,
'exec_prefix': exec_prefix,
'userbase': self.install_userbase,
'usersite': self.install_usersite,
}
self.expand_basedirs()
@ -301,6 +345,10 @@ class install (Command):
self.dump_dirs("post-expand_dirs()")
# Create directories in the home dir:
if self.user:
self.create_home_path()
# Pick the actual directory to install all modules to: either
# install_purelib or install_platlib, depending on whether this
# module distribution is pure or not. Of course, if the user
@ -315,7 +363,8 @@ class install (Command):
# Convert directories from Unix /-separated syntax to the local
# convention.
self.convert_paths('lib', 'purelib', 'platlib',
'scripts', 'data', 'headers')
'scripts', 'data', 'headers',
'userbase', 'usersite')
# Well, we're not actually fully completely finalized yet: we still
# have to deal with 'extra_path', which is the hack for allowing
@ -376,7 +425,13 @@ class install (Command):
"installation scheme is incomplete")
return
if self.home is not None:
if self.user:
if self.install_userbase is None:
raise DistutilsPlatformError(
"User base directory is not specified")
self.install_base = self.install_platbase = self.install_userbase
self.select_scheme("unix_user")
elif self.home is not None:
self.install_base = self.install_platbase = self.home
self.select_scheme("unix_home")
else:
@ -401,7 +456,13 @@ class install (Command):
def finalize_other (self): # Windows and Mac OS for now
if self.home is not None:
if self.user:
if self.install_userbase is None:
raise DistutilsPlatformError(
"User base directory is not specified")
self.install_base = self.install_platbase = self.install_userbase
self.select_scheme(os.name + "_user")
elif self.home is not None:
self.install_base = self.install_platbase = self.home
self.select_scheme("unix_home")
else:
@ -431,7 +492,7 @@ class install (Command):
for attr in attrs:
val = getattr(self, attr)
if val is not None:
if os.name == 'posix':
if os.name == 'posix' or os.name == 'nt':
val = os.path.expanduser(val)
val = subst_vars(val, self.config_vars)
setattr(self, attr, val)
@ -496,6 +557,16 @@ class install (Command):
attr = "install_" + name
setattr(self, attr, change_root(self.root, getattr(self, attr)))
def create_home_path(self):
"""Create directories under ~
"""
if not self.user:
return
home = convert_path(os.path.expanduser("~"))
for name, path in self.config_vars.iteritems():
if path.startswith(home) and not os.path.isdir(path):
self.debug_print("os.makedirs('%s', 0700)" % path)
os.makedirs(path, 0700)
# -- Command execution methods -------------------------------------

View File

@ -62,11 +62,21 @@ import sys
import os
import __builtin__
# Prefixes for site-packages; add additional prefixes like /usr/local here
PREFIXES = [sys.prefix, sys.exec_prefix]
# Enable per user site-packages directory
# set it to False to disable the feature or True to force the feature
ENABLE_USER_SITE = None
# for distutils.commands.install
USER_SITE = None
USER_BASE = None
def makepath(*paths):
dir = os.path.abspath(os.path.join(*paths))
return dir, os.path.normcase(dir)
def abs__file__():
"""Set all module' __file__ attribute to an absolute path"""
for m in sys.modules.values():
@ -77,6 +87,7 @@ def abs__file__():
except AttributeError:
continue
def removeduppaths():
""" Remove duplicate entries from sys.path along with making them
absolute"""
@ -107,6 +118,7 @@ def addbuilddir():
s = os.path.join(os.path.dirname(sys.path[-1]), s)
sys.path.append(s)
def _init_pathinfo():
"""Return a set containing all existing directory entries from sys.path"""
d = set()
@ -119,6 +131,7 @@ def _init_pathinfo():
continue
return d
def addpackage(sitedir, name, known_paths):
"""Process a .pth file within the site-packages directory:
For each line in the file, either combine it with sitedir to a path
@ -134,11 +147,11 @@ def addpackage(sitedir, name, known_paths):
f = open(fullname, "rU")
except IOError:
return
try:
with f:
for line in f:
if line.startswith("#"):
continue
if line.startswith("import ") or line.startswith("import\t"):
if line.startswith(("import ", "import\t")):
exec line
continue
line = line.rstrip()
@ -146,12 +159,11 @@ def addpackage(sitedir, name, known_paths):
if not dircase in known_paths and os.path.exists(dir):
sys.path.append(dir)
known_paths.add(dircase)
finally:
f.close()
if reset:
known_paths = None
return known_paths
def addsitedir(sitedir, known_paths=None):
"""Add 'sitedir' argument to sys.path if missing and handle .pth files in
'sitedir'"""
@ -167,48 +179,115 @@ def addsitedir(sitedir, known_paths=None):
names = os.listdir(sitedir)
except os.error:
return
names.sort()
for name in names:
if name.endswith(os.extsep + "pth"):
addpackage(sitedir, name, known_paths)
dotpth = os.extsep + "pth"
names = [name for name in names if name.endswith(dotpth)]
for name in sorted(names):
addpackage(sitedir, name, known_paths)
if reset:
known_paths = None
return known_paths
def check_enableusersite():
"""Check if user site directory is safe for inclusion
The functions tests for the command line flag (including environment var),
process uid/gid equal to effective uid/gid.
None: Disabled for security reasons
False: Disabled by user (command line option)
True: Safe and enabled
"""
if sys.flags.no_user_site:
return False
if hasattr(os, "getuid") and hasattr(os, "geteuid"):
# check process uid == effective uid
if os.geteuid() != os.getuid():
return None
if hasattr(os, "getgid") and hasattr(os, "getegid"):
# check process gid == effective gid
if os.getegid() != os.getgid():
return None
return True
def addusersitepackages(known_paths):
"""Add a per user site-package to sys.path
Each user has its own python directory with site-packages in the
home directory.
USER_BASE is the root directory for all Python versions
USER_SITE is the user specific site-packages directory
USER_SITE/.. can be used for data.
"""
global USER_BASE, USER_SITE, ENABLE_USER_SITE
env_base = os.environ.get("PYTHONUSERBASE", None)
def joinuser(*args):
return os.path.expanduser(os.path.join(*args))
#if sys.platform in ('os2emx', 'riscos'):
# # Don't know what to put here
# USER_BASE = ''
# USER_SITE = ''
if os.name == "nt":
base = os.environ.get("APPDATA") or "~"
USER_BASE = env_base if env_base else joinuser(base, "Python")
USER_SITE = os.path.join(USER_BASE,
"Python" + sys.version[0] + sys.version[2],
"site-packages")
else:
USER_BASE = env_base if env_base else joinuser("~", ".local")
USER_SITE = os.path.join(USER_BASE, "lib",
"python" + sys.version[:3],
"site-packages")
if ENABLE_USER_SITE and os.path.isdir(USER_SITE):
addsitedir(USER_SITE, known_paths)
return known_paths
def addsitepackages(known_paths):
"""Add site-packages (and possibly site-python) to sys.path"""
prefixes = [sys.prefix]
if sys.exec_prefix != sys.prefix:
prefixes.append(sys.exec_prefix)
for prefix in prefixes:
if prefix:
if sys.platform in ('os2emx', 'riscos'):
sitedirs = [os.path.join(prefix, "Lib", "site-packages")]
elif os.sep == '/':
sitedirs = [os.path.join(prefix,
"lib",
"python" + sys.version[:3],
"site-packages"),
os.path.join(prefix, "lib", "site-python")]
else:
sitedirs = [prefix, os.path.join(prefix, "lib", "site-packages")]
if sys.platform == 'darwin':
# for framework builds *only* we add the standard Apple
# locations. Currently only per-user, but /Library and
# /Network/Library could be added too
if 'Python.framework' in prefix:
home = os.environ.get('HOME')
if home:
sitedirs.append(
os.path.join(home,
'Library',
'Python',
sys.version[:3],
'site-packages'))
for sitedir in sitedirs:
if os.path.isdir(sitedir):
addsitedir(sitedir, known_paths)
return None
sitedirs = []
seen = []
for prefix in PREFIXES:
if not prefix or prefix in seen:
continue
seen.append(prefix)
if sys.platform in ('os2emx', 'riscos'):
sitedirs.append(os.path.join(prefix, "Lib", "site-packages"))
elif os.sep == '/':
sitedirs.append(os.path.join(prefix, "lib",
"python" + sys.version[:3],
"site-packages"))
sitedirs.append(os.path.join(prefix, "lib", "site-python"))
else:
sitedirs.append(prefix)
sitedirs.append(os.path.join(prefix, "lib", "site-packages"))
if sys.platform == "darwin":
# for framework builds *only* we add the standard Apple
# locations. Currently only per-user, but /Library and
# /Network/Library could be added too
if 'Python.framework' in prefix:
sitedirs.append(
os.path.expanduser(
os.path.join("~", "Library", "Python",
sys.version[:3], "site-packages")))
for sitedir in sitedirs:
if os.path.isdir(sitedir):
addsitedir(sitedir, known_paths)
return known_paths
def setBEGINLIBPATH():
@ -395,13 +474,26 @@ def execsitecustomize():
pass
def execusercustomize():
"""Run custom user specific code, if available."""
try:
import usercustomize
except ImportError:
pass
def main():
global ENABLE_USER_SITE
abs__file__()
paths_in_sys = removeduppaths()
known_paths = removeduppaths()
if (os.name == "posix" and sys.path and
os.path.basename(sys.path[-1]) == "Modules"):
addbuilddir()
paths_in_sys = addsitepackages(paths_in_sys)
if ENABLE_USER_SITE is None:
ENABLE_USER_SITE = check_enableusersite()
known_paths = addusersitepackages(known_paths)
known_paths = addsitepackages(known_paths)
if sys.platform == 'os2emx':
setBEGINLIBPATH()
setquit()
@ -410,6 +502,8 @@ def main():
aliasmbcs()
setencoding()
execsitecustomize()
if ENABLE_USER_SITE:
execusercustomize()
# Remove sys.setdefaultencoding() so that users cannot change the
# encoding after initialization. The test for presence is needed when
# this module is run as a script, because this code is executed twice.
@ -418,11 +512,54 @@ def main():
main()
def _test():
print "sys.path = ["
for dir in sys.path:
print " %r," % (dir,)
print "]"
def _script():
help = """\
%s [--user-base] [--user-site]
Without arguments print some useful information
With arguments print the value of USER_BASE and/or USER_SITE separated
by '%s'.
Exit codes with --user-base or --user-site:
0 - user site directory is enabled
1 - user site diretory is disabled by user
2 - uses site directory is disabled by super user
or for security reasons
>2 - unknown error
"""
args = sys.argv[1:]
if not args:
print "sys.path = ["
for dir in sys.path:
print " %r," % (dir,)
print "]"
print "USER_BASE: %r (%s)" % (USER_BASE,
"exists" if os.path.isdir(USER_BASE) else "doesn't exist")
print "USER_SITE: %r (%s)" % (USER_SITE,
"exists" if os.path.isdir(USER_SITE) else "doesn't exist")
print "ENABLE_USER_SITE: %r" % ENABLE_USER_SITE
sys.exit(0)
buffer = []
if '--user-base' in args:
buffer.append(USER_BASE)
if '--user-site' in args:
buffer.append(USER_SITE)
if buffer:
print os.pathsep.join(buffer)
if ENABLE_USER_SITE:
sys.exit(0)
elif ENABLE_USER_SITE is False:
sys.exit(1)
elif ENABLE_USER_SITE is None:
sys.exit(2)
else:
sys.exit(3)
else:
import textwrap
print textwrap.dedent(help % (sys.argv[0], os.pathsep))
sys.exit(10)
if __name__ == '__main__':
_test()
_script()

View File

@ -10,6 +10,7 @@ import __builtin__
import os
import sys
import encodings
import subprocess
# Need to make sure to not import 'site' if someone specified ``-S`` at the
# command-line. Detect this by just making sure 'site' has not been imported
# already.
@ -18,6 +19,11 @@ if "site" in sys.modules:
else:
raise TestSkipped("importation of site.py suppressed")
if not os.path.isdir(site.USER_SITE):
# need to add user site directory for tests
os.makedirs(site.USER_SITE)
site.addsitedir(site.USER_SITE)
class HelperFunctionsTests(unittest.TestCase):
"""Tests for helper functions.
@ -30,7 +36,7 @@ class HelperFunctionsTests(unittest.TestCase):
"""Save a copy of sys.path"""
self.sys_path = sys.path[:]
def tearDown(self):
"""Restore sys.path"""
sys.path = self.sys_path
@ -90,6 +96,33 @@ class HelperFunctionsTests(unittest.TestCase):
finally:
pth_file.cleanup()
def test_s_option(self):
usersite = site.USER_SITE
self.assert_(usersite in sys.path)
rc = subprocess.call([sys.executable, '-c',
'import sys; sys.exit("%s" in sys.path)' % usersite])
self.assertEqual(rc, 1)
rc = subprocess.call([sys.executable, '-s', '-c',
'import sys; sys.exit("%s" in sys.path)' % usersite])
self.assertEqual(rc, 0)
env = os.environ.copy()
env["PYTHONNOUSERSITE"] = "1"
rc = subprocess.call([sys.executable, '-c',
'import sys; sys.exit("%s" in sys.path)' % usersite],
env=env)
self.assertEqual(rc, 0)
env = os.environ.copy()
env["PYTHONUSERBASE"] = "/tmp"
rc = subprocess.call([sys.executable, '-c',
'import sys, site; sys.exit(site.USER_BASE.startswith("/tmp"))'],
env=env)
self.assertEqual(rc, 1)
class PthFile(object):
"""Helper class for handling testing of .pth files"""

View File

@ -20,6 +20,8 @@ Core and builtins
- Patch #2617: Reserved -J and -X arguments for Jython, IronPython and other
implementations of Python.
- Implemented PEP 370: Per user site-packages directory
Extension Modules
-----------------

View File

@ -40,7 +40,7 @@ static char **orig_argv;
static int orig_argc;
/* command line options */
#define BASE_OPTS "3bBc:dEhiJm:OQ:StuUvVW:xX?"
#define BASE_OPTS "3bBc:dEhiJm:OQ:sStuUvVW:xX?"
#ifndef RISCOS
#define PROGRAM_OPTS BASE_OPTS
@ -72,6 +72,7 @@ static char *usage_2 = "\
-O : optimize generated bytecode slightly; also PYTHONOPTIMIZE=x\n\
-OO : remove doc-strings in addition to the -O optimizations\n\
-Q arg : division options: -Qold (default), -Qwarn, -Qwarnall, -Qnew\n\
-s : don't add user site directory to sys.path; also PYTHONNOUSERSITE\n\
-S : don't imply 'import site' on initialization\n\
-t : issue warnings about inconsistent tab usage (-tt: issue errors)\n\
";
@ -359,6 +360,10 @@ Py_Main(int argc, char **argv)
Py_DontWriteBytecodeFlag++;
break;
case 's':
Py_NoUserSiteDirectory++;
break;
case 'S':
Py_NoSiteFlag++;
break;
@ -431,6 +436,10 @@ Py_Main(int argc, char **argv)
(p = Py_GETENV("PYTHONUNBUFFERED")) && *p != '\0')
unbuffered = 1;
if (!Py_NoUserSiteDirectory &&
(p = Py_GETENV("PYTHONNOUSERSITE")) && *p != '\0')
Py_NoUserSiteDirectory = 1;
if (command == NULL && module == NULL && _PyOS_optind < argc &&
strcmp(argv[_PyOS_optind], "-") != 0)
{

View File

@ -82,6 +82,7 @@ int Py_IgnoreEnvironmentFlag; /* e.g. PYTHONPATH, PYTHONHOME */
on the command line, and is used in 2.2 by ceval.c to make all "/" divisions
true divisions (which they will be in 2.3). */
int _Py_QnewFlag = 0;
int Py_NoUserSiteDirectory = 0; /* for -s and site.py */
/* PyModule_GetWarningsModule is no longer necessary as of 2.6
since _warnings is builtin. This API should not be used. */

View File

@ -1101,7 +1101,7 @@ svnversion_init(void)
shortbranch[len] = '\0';
}
else {
Py_FatalError("bad HeadURL");
Py_FatalError("bad HeadURL");
return;
}
@ -1158,7 +1158,7 @@ static PyStructSequence_Field flags_fields[] = {
{"interactive", "-i"},
{"optimize", "-O or -OO"},
{"dont_write_bytecode", "-B"},
/* {"no_user_site", "-s"}, */
{"no_user_site", "-s"},
{"no_site", "-S"},
{"ignore_environment", "-E"},
{"tabcheck", "-t or -tt"},
@ -1178,9 +1178,9 @@ static PyStructSequence_Desc flags_desc = {
flags__doc__, /* doc */
flags_fields, /* fields */
#ifdef RISCOS
14
15
#else
13
14
#endif
};
@ -1205,7 +1205,7 @@ make_flags(void)
SetFlag(Py_InteractiveFlag);
SetFlag(Py_OptimizeFlag);
SetFlag(Py_DontWriteBytecodeFlag);
/* SetFlag(Py_NoUserSiteDirectory); */
SetFlag(Py_NoUserSiteDirectory);
SetFlag(Py_NoSiteFlag);
SetFlag(Py_IgnoreEnvironmentFlag);
SetFlag(Py_TabcheckFlag);