cpython/Lib/site.py
Barry Warsaw 27ebd9abce
gh-150228: Improve the PEP 829 batch processing APIs (#150542)
* gh-150228: Improve the PEP 829 batch processing APIs

As previously discussed with @ncoghlan and approved for 3.15b2 by @hugovk,
this implements the batch processing APIs for addsitedir() and friends.  We
remove the `defer_processing_start_files` flag which required some implicit
module global state, and promote StartupState to the public documented API.

This also moves the bulk of the module global functions into methods of the
`StartupState` class, so it removes the awkward APIs in 3.15b1.  Now, instances
of this class are an accumulator for startup state, using `StartupState.process()`
to process them.  Callers can now batch up startup state themselves by using
the methods on this class.  The module global functions are shims for this
which preserve the legacy APIs and semantics using the new state class.

This PR also fixes the interleaving regression identified by @ncoghlan in the
same issue.  Now, .pth file sys.path extensions are added to sys.path after
the sitedir that the .pth file is found in, restoring the legacy behavior.

Along the way, I've made a lot of improvements to function docstrings,
site.rst documentation, and comments in the code explaining what's going on.

* Add a note that if known_paths is provided to StartupState.__init__(), it
  will get mutated in place.
* Improve some conditional flows.
* Improve some comments.
* Improve the what's new entry.

* Make test_impl_exec_imports_suppressed_by_matching_start() more robust

Based on PR comment, we need to read both the .pth and .start files, and prove
that the .pth file's import line (which passes a bigger increment) is not
called, but the .start file's entry point (which uses the default increment)
is called.

* As per review, move some methods to the private API

_read_pth_file() and _read_start_file() are not intended to be part of the
public API surface outside of the site module, so even though they are used by
methods outside of the StartupState class, make them privately named.

* Resolve several review feedbacks

* Move a `versionadded`
* Better list comprehension formatting (use the output from
  `ruff format --line-length 78`)

* Add docs for site.makepath() and point the case-normalization requirement to
  this utility function.
* Note that StartupState.process() is not idempotent.

* Address another feedback comment

This time, we get rid of the legacy implementation `reset` local, which was
always difficult to understand, and just implement a return value based on the
processing mode selected.

* Changes based on gh-150228 review

The comment by @encukou that started this change:

```
I still see two red flags here though: an argument that doesn't combine with
other arguments, and (another instance of) changing the return type based on
an argument.

Did you consider adding a StartupState.addsitedir(sitedir) method, instead of
the startup_state argument?
```

As it turns out, this is an even cleaner design.  By moving the bulk of the
previous module global functions into `StartupState` methods, we can get rid
of all the awkward `startup_state` keyword-only arguments which conflict
with `known_path` (Petr's first point).  We can also get rid of the
return value dichotomy (Petr's second point) because now we can preserve
exactly the Python 3.14 API in the module global functions, and implement
the better APIs in the class methods.  We also generally don't have to
pass around `process_known_sitedirs`.

Now the following module global functions are essentially shims around
class methods:

* site.addsitedir() -> StartupState.addsitedir()
* site.addusersitepackages() -> StartupState.addusersitepackages()
* site.addsitepackages() -> StartupState.addsitepackages()
* Additional minor changes
* Remove a now unused parameter


Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-06-01 18:43:18 -07:00

1148 lines
41 KiB
Python

"""Append module search paths for third-party packages to sys.path.
****************************************************************
* This module is automatically imported during initialization. *
****************************************************************
This will append site-specific paths to the module search path. On
Unix (including Mac OSX), it starts with sys.prefix and
sys.exec_prefix (if different) and appends
lib/python<version>/site-packages.
On other platforms (such as Windows), it tries each of the
prefixes directly, as well as with lib/site-packages appended. The
resulting directories, if they exist, are appended to sys.path, and
also inspected for path configuration files.
If a file named "pyvenv.cfg" exists one directory above sys.executable,
sys.prefix and sys.exec_prefix are set to that directory and
it is also checked for site-packages (sys.base_prefix and
sys.base_exec_prefix will always be the "real" prefixes of the Python
installation). If "pyvenv.cfg" (a bootstrap configuration file) contains
the key "include-system-site-packages" set to "true" (case-insensitive),
the system-level prefixes will still also be searched for site-packages;
otherwise they won't.
Two kinds of configuration files are processed in each site-packages
directory:
- <name>.pth files extend sys.path with additional directories (one per
line). Lines starting with "import" are deprecated (see PEP 829).
- <name>.start files specify startup entry points using the pkg.mod:callable
syntax. These are resolved via pkgutil.resolve_name() and called with no
arguments.
When called from main(), all .pth path extensions are applied before any
.start entry points are executed, ensuring that paths are available before
startup code runs.
See the documentation for the site module for full details:
https://docs.python.org/3/library/site.html
"""
import sys
import os
import builtins
import _sitebuiltins
import _io as io
import stat
import errno
lazy import locale
lazy import pkgutil
lazy import traceback
lazy import warnings
# 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
# These values are initialized by the getuserbase() and getusersitepackages()
# functions, through the main() function when Python starts.
USER_SITE = None
USER_BASE = None
def _trace(message, exc=None):
if sys.flags.verbose:
_print_error(message, exc)
def _print_error(message, exc=None):
"""Print an error message to stderr, optionally with a formatted traceback."""
print(message, file=sys.stderr)
if exc is not None:
for record in traceback.format_exception(exc):
for line in record.splitlines():
print(' ' + line, file=sys.stderr)
def _warn(*args, **kwargs):
warnings.warn(*args, **kwargs)
def _warn_future_us(message, remove):
# Don't call warnings._deprecated() directly because we're lazily importing warnings and don't
# want to have to trigger an eager import if it's not necessary. Startup time matters a lot
# here and warnings isn't cheap! This inlines the check from
# warnings._py_warnings._deprecated().
_version = sys.version_info
if (_version[:2] > remove) or (_version[:2] == remove and _version[3] != "alpha"):
warnings._deprecated(message, remove=remove)
def makepath(*paths):
dir = os.path.join(*paths)
try:
dir = os.path.abspath(dir)
except OSError:
pass
return dir, os.path.normcase(dir)
def abs_paths():
"""Set __file__ to an absolute path."""
for m in set(sys.modules.values()):
loader_module = None
try:
loader_module = m.__loader__.__module__
except AttributeError:
try:
loader_module = m.__spec__.loader.__module__
except AttributeError:
pass
if loader_module not in {'_frozen_importlib', '_frozen_importlib_external'}:
continue # don't mess with a PEP 302-supplied __file__
try:
m.__file__ = os.path.abspath(m.__file__)
except (AttributeError, OSError, TypeError):
pass
def removeduppaths():
""" Remove duplicate entries from sys.path along with making them
absolute"""
# This ensures that the initial path provided by the interpreter contains
# only absolute pathnames, even if we're running from the build directory.
L = []
known_paths = set()
for dir in sys.path:
# Filter out duplicate paths (on case-insensitive file systems also
# if they only differ in case); turn relative paths into absolute
# paths.
dir, dircase = makepath(dir)
if dircase not in known_paths:
L.append(dir)
known_paths.add(dircase)
sys.path[:] = L
return known_paths
def _init_pathinfo():
"""Return a set containing all existing file system items from sys.path."""
d = set()
for item in sys.path:
try:
if os.path.exists(item):
_, itemcase = makepath(item)
d.add(itemcase)
except TypeError:
continue
return d
# PEP 829 implementation notes.
#
# Startup information (.pth and .start file information) can be processed in
# implicit or explicit batches. Implicit batches are self-contained
# site.addsitedir() calls: they create a per-call StartupState, populate it
# from the site directory's .pth and .start files, run process() on it, and
# then throw the state away.
#
# main() needs different semantics: it accumulates state across multiple
# StartupState.addsitedir() calls (user-site plus all global site-packages) so
# that every sys.path extension is visible *before* any startup code (.start
# entry points and .pth import lines) runs. Callers can opt into the same
# behavior by creating a StartupState directly and calling its addsitedir(),
# addusersitepackages(), and addsitepackages() methods, then invoking
# process() once at the end of the batch.
#
# Here's the CRITICAL reentrancy invariant: recursive site.addsitedir() calls
# reached from a .start entry point or an exec'd .pth import line must not
# mutate the StartupState currently being processed. Reentrant calls reach
# the module-level site.addsitedir() shim, which always builds a fresh
# per-call state.
def _read_pthstart_file(sitedir, name, suffix):
"""Parse a .start or .pth file and return (lines, filename).
On success, ``lines`` is a (possibly empty) list of the file's lines.
On failure (file missing, hidden, unreadable, or .start with bad
encoding), ``lines`` is ``None`` so callers can distinguish a
successfully-read empty file from one that could not be read.
"""
filename = os.path.join(sitedir, name)
_trace(f"Reading startup configuration file: {filename}")
try:
st = os.lstat(filename)
except OSError as exc:
_trace(f"Cannot stat {filename!r}", exc)
return None, filename
if ((getattr(st, 'st_flags', 0) & stat.UF_HIDDEN) or
(getattr(st, 'st_file_attributes', 0) & stat.FILE_ATTRIBUTE_HIDDEN)):
_trace(f"Skipping hidden {suffix} file: {filename!r}")
return None, filename
_trace(f"Processing {suffix} file: {filename!r}")
try:
with io.open_code(filename) as f:
raw_content = f.read()
except OSError as exc:
_trace(f"Cannot read {filename!r}", exc)
return None, filename
try:
# Accept BOM markers in .start and .pth files as we do in source files
# (Windows PowerShell 5.1 makes it hard to emit UTF-8 files without a BOM).
content = raw_content.decode("utf-8-sig")
except UnicodeDecodeError:
_trace(f"Cannot read {filename!r} as UTF-8.")
# For .pth files only, and then only until Python 3.20, fall back to
# locale encoding for backward compatibility.
_warn_future_us(
".pth files decoded to locale encoding as a fallback",
remove=(3, 20)
)
if suffix == ".pth":
content = raw_content.decode(locale.getencoding())
_trace(f"Using fallback encoding {locale.getencoding()!r}")
else:
return None, filename
return content.splitlines(), filename
class StartupState:
"""Per-batch accumulator for .pth and .start file processing.
A StartupState collects sys.path extensions, deprecated .pth import lines,
and .start entry points read from one or more site-packages directories.
Calling process() applies them in PEP 829 order: paths are added to
sys.path first, then import lines from .pth files (skipping any with a
matching .start), then entry points from .start files.
State lives entirely on the instance; there is no module-level pending
state. This is what makes the module reentrancy-safe: a site.addsitedir()
call reached recursively from an exec'd import line or a .start entry
point operates on a different StartupState than the one being processed by
the outer call.
The internal data is intentionally private. The lower-level write
methods (_record_sitedir(), _read_pth_file(), _read_start_file()) are
private to the site module; the public surface is addsitedir(),
addusersitepackages(), addsitepackages(), and process().
"""
__slots__ = (
'_known_paths',
'_processed_sitedirs',
'_path_entries',
'_importexecs',
'_entrypoints',
)
def __init__(self, known_paths=None):
"""Create an independent startup state.
*known_paths* is a set of case-normalized paths already present
on sys.path, used to avoid duplicate path entries. When None
(the default), it is initialized from the current sys.path.
A caller-supplied set is stored by reference and mutated in place
as new paths are recorded; pass a fresh set per StartupState if
isolation across instances is required.
"""
self._known_paths = (
_init_pathinfo()
if known_paths is None
else known_paths)
self._processed_sitedirs = set()
# The sys.path append ledger. This is a list of 2-tuples of the form
# (pthfile, path) where `pthfile` is the .pth file which is extending
# the path, and `path` is the directory to add to sys.path. Note that
# to preserve the interleaving semantics (i.e. .pth file paths are
# added after the sitedir in which the .pth file is found), `path`
# could be a sitedir, in which case `pthfile` will always be None.
self._path_entries = []
# Both dicts map "<full path to .pth or .start file>" -> list
# of items collected from that file. Mapping by filename lets us
# cross-reference a .pth and its matching .start (PEP 829 import
# suppression rule) and lets _print_error report the source file
# when an entry fails.
self._importexecs = {}
self._entrypoints = {}
def addsitedir(self, sitedir):
"""Add a site directory and accumulate its .pth and .start startup data.
Read the .pth and .start files in *sitedir* and record their
sys.path extensions, deprecated .pth import lines, and .start entry
points on this state. The recorded data is not applied until
process() is called.
Typically used to batch multiple site directories before a single
process() call, so that every sys.path extension is visible before
any startup code runs. Reentrant calls reached from a .start entry
point or an exec'd .pth import line must not mutate the state
currently being processed; for those cases, use site.addsitedir()
instead, which always creates a fresh per-call state.
"""
self._addsitedir(sitedir, process_known_sitedirs=True)
def addusersitepackages(self):
"""Add the per-user site-packages directory, if enabled.
The user site directory is added only when user site-packages are
enabled and the directory exists. Its startup data is accumulated
for later processing by process().
"""
_trace("Processing user site-packages")
user_site = getusersitepackages()
if ENABLE_USER_SITE and os.path.isdir(user_site):
self.addsitedir(user_site)
def addsitepackages(self, prefixes=None):
"""Add global site-packages directories, if they exist.
Site-packages directories are computed from *prefixes*, or from the
global PREFIXES when *prefixes* is None. Each directory's startup
data is accumulated for later processing by process().
"""
_trace("Processing global site-packages")
for sitedir in getsitepackages(prefixes):
if os.path.isdir(sitedir):
self.addsitedir(sitedir)
def _addsitedir(self, sitedir, *, process_known_sitedirs):
"""Internal addsitedir() implementation with full dedup control.
The public addsitedir() always uses process_known_sitedirs=True
(gh-149819 semantics). The module-level legacy known_paths shim
uses process_known_sitedirs=False to preserve 3.14 idempotency
(gh-75723).
"""
sitedir = self._record_sitedir(
sitedir, process_known_sitedirs=process_known_sitedirs)
if sitedir is None:
return
try:
names = os.listdir(sitedir)
except OSError:
return
# The following phases are defined by PEP 829.
# Phases 1-3: Read .pth files, accumulating paths and import lines.
pth_names = sorted(
name for name in names
if name.endswith(".pth") and not name.startswith(".")
)
for name in pth_names:
self._read_pth_file(sitedir, name)
# Phases 6-7: Discover .start files and accumulate their entry points.
# Import lines from .pth files with a matching .start file are
# discarded at flush time by _exec_imports().
start_names = sorted(
name for name in names
if name.endswith(".start") and not name.startswith(".")
)
for name in start_names:
self._read_start_file(sitedir, name)
def _record_sitedir(self, sitedir, *, process_known_sitedirs=True):
sitedir, sitedircase = makepath(sitedir)
# Have we already processed this sitedir?
if sitedircase in self._processed_sitedirs:
return None
# In legacy known_paths mode, a known sitedir means its startup files
# were already processed by an earlier addsitedir() call, so skip it
# to preserve idempotency (gh-75723). In explicit StartupState mode,
# known_paths only tracks sys.path entries; a sitedir may already be
# on sys.path (for example from $PYTHONPATH, gh-149819) but still need
# its .pth and .start files processed once. The separate
# _processed_sitedirs set is what lets explicit batches distinguish
# "already on sys.path" from "startup files already read".
if not process_known_sitedirs and sitedircase in self._known_paths:
return None
# Record that we've processed this sitedir.
self._processed_sitedirs.add(sitedircase)
if sitedircase not in self._known_paths:
self._known_paths.add(sitedircase)
# Add the sitedir to the sys.path extension ledger. There is no
# .pth file to record.
self._path_entries.append((None, sitedir))
return sitedir
def _read_pth_file(self, sitedir, name):
"""Parse a .pth file, accumulating sys.path extensions and import lines.
Errors on individual lines do not abort processing of the rest of
the file (PEP 829). Per-batch deduplication is done against
self._known_paths: any path already in it is skipped, and newly
accepted paths are added to it so that subsequent .pth files in
the same batch don't add them more than once.
"""
lines, filename = _read_pthstart_file(sitedir, name, ".pth")
if lines is None:
return
for n, line in enumerate(lines, 1):
line = line.strip()
if not line or line.startswith("#"):
continue
# In Python 3.18 and 3.19, `import` lines are silently
# ignored. In Python 3.20 and beyond, issue a warning when
# `import` lines in .pth files are detected.
if line.startswith(("import ", "import\t")):
_warn_future_us(
"import lines in .pth files are silently ignored",
remove=(3, 18),
)
_warn_future_us(
"import lines in .pth files are noisily ignored",
remove=(3, 20),
)
self._importexecs.setdefault(filename, []).append(line)
continue
try:
dir_, dircase = makepath(sitedir, line)
except Exception as exc:
_trace(f"Error in {filename!r}, line {n:d}: {line!r}", exc)
continue
# PEP 829 dedup: skip paths already seen in this batch.
if dircase in self._known_paths:
_trace(
f"In {filename!r}, line {n:d}: "
f"skipping duplicate sys.path entry: {dir_}"
)
else:
# Add this directory to the sys.path extension ledger, while
# also recording the .pth file it was found in.
self._path_entries.append((filename, dir_))
self._known_paths.add(dircase)
def _read_start_file(self, sitedir, name):
"""Parse a .start file for a list of entry point strings."""
lines, filename = _read_pthstart_file(sitedir, name, ".start")
if lines is None:
return
# PEP 829: the *presence* of a matching .start file disables `import`
# line processing in the matched .pth file, regardless of whether this
# .start file contains any entry points. Register the filename as a
# key now so an empty (or comment-only) .start file still suppresses.
entrypoints = self._entrypoints.setdefault(filename, [])
for n, line in enumerate(lines, 1):
line = line.strip()
if not line or line.startswith("#"):
continue
# Syntax validation is deferred to entry point execution
# time, where pkgutil.resolve_name(strict=True) enforces the
# pkg.mod:callable form.
entrypoints.append(line)
def process(self):
"""Apply accumulated state in PEP 829 order.
Phase order matters: all .pth path extensions are applied to
sys.path *before* any import line or .start entry point runs, so
that an entry point may live in a module reachable only via a
.pth-extended path.
"""
self._extend_syspath()
self._exec_imports()
self._execute_start_entrypoints()
def _extend_syspath(self):
# Duplicate path-extension specifications have already been filtered
# out upstream across .pth files within this batch (via known_paths),
# and ledger entries are already abspath/normpath'd. .pth-derived
# entries (filename is not None) are existence-checked and skipped
# with an error if missing. Sitedir entries (filename is None) are
# appended unconditionally: legacy addsitedir() added the sitedir to
# sys.path before attempting to list it, so an unreadable or
# non-existent sitedir still landed on sys.path. Deferring the
# append to here preserves that contract.
for filename, dir_ in self._path_entries:
# As a backstop, known_paths may not have been seeded from sys.path
# (callers can pass an empty set), and multiple StartupState
# instances against the same sys.path don't share state, so always
# do a final anti-duplication check.
if dir_ in sys.path:
continue
if filename is None or os.path.exists(dir_):
if filename is not None:
_trace(f"Extending sys.path with {dir_} from {filename}")
sys.path.append(dir_)
else:
_print_error(
f"In {filename}: {dir_} does not exist; "
f"skipping sys.path append"
)
def _exec_imports(self):
# For each `import` line we've seen in a .pth file, exec() it in
# order, unless the .pth has a matching .start file in this same
# batch. In that case, PEP 829 says the import lines are
# suppressed in favor of the .start's entry points.
for filename, imports in self._importexecs.items():
# Given "/path/to/foo.pth", check whether "/path/to/foo.start" was
# registered in this same batch.
name, dot, pth = filename.rpartition(".")
assert dot == "." and pth == "pth", (
f"Bad startup filename: {filename}"
)
if f"{name}.start" in self._entrypoints:
_trace(
f"import lines in {filename} are suppressed "
f"due to matching {name}.start file."
)
continue
_trace(
f"import lines in {filename} are deprecated, "
f"use entry points in a {name}.start file instead."
)
for line in imports:
try:
_trace(f"Exec'ing from {filename}: {line}")
exec(line)
except Exception as exc:
_print_error(
f"Error in import line from {filename}: {line}",
exc,
)
def _execute_start_entrypoints(self):
# Resolve each entry point string to a callable via
# pkgutil.resolve_name(strict=True), which both validates the
# required pkg.mod:callable form and performs the import in one
# step, then call it with no arguments.
for filename, entrypoints in self._entrypoints.items():
for entrypoint in entrypoints:
try:
_trace(
f"Executing entry point: {entrypoint} from {filename}"
)
callable_ = pkgutil.resolve_name(entrypoint, strict=True)
except ValueError as exc:
_print_error(
f"Invalid entry point syntax in {filename}: "
f"{entrypoint!r}",
exc,
)
except Exception as exc:
_print_error(
f"Error resolving entry point {entrypoint} "
f"from {filename}",
exc,
)
else:
try:
callable_()
except Exception as exc:
_print_error(
f"Error in entry point {entrypoint} from {filename}",
exc,
)
def addpackage(sitedir, name, known_paths):
"""Process a .pth file within the site-packages directory."""
if known_paths is None:
known_paths = _init_pathinfo()
reset = True
else:
reset = False
state = StartupState(known_paths)
state._read_pth_file(sitedir, name)
state.process()
return None if reset else known_paths
def addsitedir(sitedir, known_paths=None):
"""Add a site directory and process its startup files.
For batched processing across multiple site directories, build a
StartupState explicitly and call StartupState.addsitedir() on it; that
defers .pth/.start processing until a single StartupState.process() call.
"""
_trace(f"Adding directory: {sitedir!r}")
if known_paths is None:
state = StartupState(_init_pathinfo())
state.addsitedir(sitedir)
else:
# Preserve gh-75723 idempotency for legacy known_paths mode: a
# sitedir already present in known_paths is skipped, not reprocessed.
state = StartupState(known_paths)
state._addsitedir(sitedir, process_known_sitedirs=False)
state.process()
return known_paths
def check_enableusersite():
"""Check if user site directory is safe for inclusion
The function 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
# NOTE: sysconfig and it's dependencies are relatively large but site module
# needs very limited part of them.
# To speedup startup time, we have copy of them.
#
# See https://bugs.python.org/issue29585
# Copy of sysconfig._get_implementation()
def _get_implementation():
return 'Python'
# Copy of sysconfig._getuserbase()
def _getuserbase():
env_base = os.environ.get("PYTHONUSERBASE", None)
if env_base:
return env_base
# Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
return None
def joinuser(*args):
return os.path.expanduser(os.path.join(*args))
if os.name == "nt":
base = os.environ.get("APPDATA") or "~"
return joinuser(base, _get_implementation())
if sys.platform == "darwin" and sys._framework:
return joinuser("~", "Library", sys._framework,
"%d.%d" % sys.version_info[:2])
return joinuser("~", ".local")
# Same to sysconfig.get_path('purelib', os.name+'_user')
def _get_path(userbase):
version = sys.version_info
if hasattr(sys, 'abiflags') and 't' in sys.abiflags:
abi_thread = 't'
else:
abi_thread = ''
implementation = _get_implementation()
implementation_lower = implementation.lower()
if os.name == 'nt':
ver_nodot = sys.winver.replace('.', '')
return f'{userbase}\\{implementation}{ver_nodot}\\site-packages'
if sys.platform == 'darwin' and sys._framework:
return f'{userbase}/lib/{implementation_lower}/site-packages'
return f'{userbase}/lib/{implementation_lower}{version[0]}.{version[1]}{abi_thread}/site-packages'
def getuserbase():
"""Returns the `user base` directory path.
The `user base` directory can be used to store data. If the global
variable ``USER_BASE`` is not initialized yet, this function will also set
it.
"""
global USER_BASE
if USER_BASE is None:
USER_BASE = _getuserbase()
return USER_BASE
def getusersitepackages():
"""Returns the user-specific site-packages directory path.
If the global variable ``USER_SITE`` is not initialized yet, this
function will also set it.
"""
global USER_SITE, ENABLE_USER_SITE
userbase = getuserbase() # this will also set USER_BASE
if USER_SITE is None:
if userbase is None:
ENABLE_USER_SITE = False # disable user site and return None
else:
USER_SITE = _get_path(userbase)
return USER_SITE
def addusersitepackages(known_paths):
"""Add the per-user site-packages directory, if enabled.
The user site directory is added only when user site-packages are enabled
and the directory exists. Return *known_paths*, updated with any paths
added by addsitedir().
"""
state = StartupState(known_paths)
state.addusersitepackages()
state.process()
return known_paths
def getsitepackages(prefixes=None):
"""Returns a list containing all global site-packages directories.
For each directory present in ``prefixes`` (or the global ``PREFIXES``),
this function will find its `site-packages` subdirectory depending on the
system environment, and will return a list of full paths.
"""
sitepackages = []
seen = set()
if prefixes is None:
prefixes = PREFIXES
for prefix in prefixes:
if not prefix or prefix in seen:
continue
seen.add(prefix)
implementation = _get_implementation().lower()
ver = sys.version_info
if hasattr(sys, 'abiflags') and 't' in sys.abiflags:
abi_thread = 't'
else:
abi_thread = ''
if os.sep == '/':
libdirs = [sys.platlibdir]
if sys.platlibdir != "lib":
libdirs.append("lib")
for libdir in libdirs:
path = os.path.join(prefix, libdir,
f"{implementation}{ver[0]}.{ver[1]}{abi_thread}",
"site-packages")
sitepackages.append(path)
else:
sitepackages.append(prefix)
sitepackages.append(os.path.join(prefix, "Lib", "site-packages"))
return sitepackages
def addsitepackages(known_paths, prefixes=None):
"""Add global site-packages directories, if they exist.
Site-packages directories are computed from *prefixes*, or from the global
prefixes when *prefixes* is None. Return *known_paths*, updated with any
paths added by addsitedir().
"""
state = StartupState(known_paths)
state.addsitepackages(prefixes)
state.process()
return known_paths
def setquit():
"""Define new builtins 'quit' and 'exit'.
These are objects which make the interpreter exit when called.
The repr of each object contains a hint at how it works.
"""
if os.sep == '\\':
eof = 'Ctrl-Z plus Return'
else:
eof = 'Ctrl-D (i.e. EOF)'
builtins.quit = _sitebuiltins.Quitter('quit', eof)
builtins.exit = _sitebuiltins.Quitter('exit', eof)
def setcopyright():
"""Set 'copyright' and 'credits' in builtins"""
builtins.copyright = _sitebuiltins._Printer("copyright", sys.copyright)
builtins.credits = _sitebuiltins._Printer("credits", """\
Thanks to CWI, CNRI, BeOpen, Zope Corporation, the Python Software
Foundation, and a cast of thousands for supporting Python
development. See www.python.org for more information.""")
files, dirs = [], []
# Not all modules are required to have a __file__ attribute. See
# PEP 420 for more details.
here = getattr(sys, '_stdlib_dir', None)
if not here and hasattr(os, '__file__'):
here = os.path.dirname(os.__file__)
if here:
files.extend(["LICENSE.txt", "LICENSE"])
dirs.extend([os.path.join(here, os.pardir), here, os.curdir])
builtins.license = _sitebuiltins._Printer(
"license",
"See https://www.python.org/psf/license/",
files, dirs)
def sethelper():
builtins.help = _sitebuiltins._Helper()
def gethistoryfile():
"""Check if the PYTHON_HISTORY environment variable is set and define
it as the .python_history file. If PYTHON_HISTORY is not set, use the
default .python_history file.
"""
if not sys.flags.ignore_environment:
history = os.environ.get("PYTHON_HISTORY")
if history:
return history
return os.path.join(os.path.expanduser('~'),
'.python_history')
def enablerlcompleter():
"""Enable default readline configuration on interactive prompts, by
registering a sys.__interactivehook__.
"""
sys.__interactivehook__ = register_readline
def register_readline():
"""Configure readline completion on interactive prompts.
If the readline module can be imported, the hook will set the Tab key
as completion key and register ~/.python_history as history file.
This can be overridden in the sitecustomize or usercustomize module,
or in a PYTHONSTARTUP file.
"""
if not sys.flags.ignore_environment:
PYTHON_BASIC_REPL = os.getenv("PYTHON_BASIC_REPL")
else:
PYTHON_BASIC_REPL = False
import atexit
try:
try:
import readline
except ImportError:
readline = None
else:
import rlcompleter # noqa: F401
except ImportError:
return
try:
if PYTHON_BASIC_REPL:
CAN_USE_PYREPL = False
else:
original_path = sys.path
sys.path = [p for p in original_path if p != '']
try:
import _pyrepl.readline
if os.name == "nt":
import _pyrepl.windows_console
console_errors = (_pyrepl.windows_console._error,)
else:
import _pyrepl.unix_console
console_errors = _pyrepl.unix_console._error
from _pyrepl.main import CAN_USE_PYREPL
except ModuleNotFoundError:
CAN_USE_PYREPL = False
finally:
sys.path = original_path
except ImportError:
return
if readline is not None:
# Reading the initialization (config) file may not be enough to set a
# completion key, so we set one first and then read the file.
if readline.backend == 'editline':
readline.parse_and_bind('bind ^I rl_complete')
else:
readline.parse_and_bind('tab: complete')
try:
readline.read_init_file()
except OSError:
# An OSError here could have many causes, but the most likely one
# is that there's no .inputrc file (or .editrc file in the case of
# Mac OS X + libedit) in the expected location. In that case, we
# want to ignore the exception.
pass
if readline is None or readline.get_current_history_length() == 0:
# If no history was loaded, default to .python_history,
# or PYTHON_HISTORY.
# The guard is necessary to avoid doubling history size at
# each interpreter exit when readline was already configured
# through a PYTHONSTARTUP hook, see:
# http://bugs.python.org/issue5845#msg198636
history = gethistoryfile()
if CAN_USE_PYREPL:
readline_module = _pyrepl.readline
exceptions = (OSError, *console_errors)
else:
if readline is None:
return
readline_module = readline
exceptions = OSError
try:
readline_module.read_history_file(history)
except exceptions:
pass
def write_history():
try:
readline_module.write_history_file(history)
except FileNotFoundError, PermissionError:
# home directory does not exist or is not writable
# https://bugs.python.org/issue19891
pass
except OSError:
if errno.EROFS:
pass # gh-128066: read-only file system
else:
raise
atexit.register(write_history)
def venv(known_paths):
"""Process pyvenv.cfg and add the venv site-packages, if applicable."""
state = StartupState(known_paths)
_venv(state)
state.process()
return known_paths
def _venv(state):
"""State-driven implementation of venv(); used by main() for batching."""
global PREFIXES, ENABLE_USER_SITE
env = os.environ
if sys.platform == 'darwin' and '__PYVENV_LAUNCHER__' in env:
executable = sys._base_executable = os.environ['__PYVENV_LAUNCHER__']
else:
executable = sys.executable
exe_dir = os.path.dirname(os.path.abspath(executable))
site_prefix = os.path.dirname(exe_dir)
sys._home = None
conf_basename = 'pyvenv.cfg'
candidate_conf = next(
(
conffile for conffile in (
os.path.join(exe_dir, conf_basename),
os.path.join(site_prefix, conf_basename)
)
if os.path.isfile(conffile)
),
None
)
if candidate_conf:
virtual_conf = candidate_conf
system_site = "true"
# Issue 25185: Use UTF-8, as that's what the venv module uses when
# writing the file.
with open(virtual_conf, encoding='utf-8') as f:
for line in f:
if '=' in line:
key, _, value = line.partition('=')
key = key.strip().lower()
value = value.strip()
if key == 'include-system-site-packages':
system_site = value.lower()
elif key == 'home':
sys._home = value
if sys.prefix != site_prefix:
_warn(
f'Unexpected value in sys.prefix, expected {site_prefix}, got {sys.prefix}',
RuntimeWarning)
if sys.exec_prefix != site_prefix:
_warn(
f'Unexpected value in sys.exec_prefix, expected {site_prefix}, got {sys.exec_prefix}',
RuntimeWarning)
# Doing this here ensures venv takes precedence over user-site.
state.addsitepackages([sys.prefix])
if system_site == "true":
PREFIXES += [sys.base_prefix, sys.base_exec_prefix]
else:
ENABLE_USER_SITE = False
def execsitecustomize():
"""Run custom site specific code, if available."""
try:
try:
import sitecustomize # noqa: F401
except ImportError as exc:
if exc.name == 'sitecustomize':
pass
else:
raise
except Exception as err:
if sys.flags.verbose:
sys.excepthook(*sys.exc_info())
else:
sys.stderr.write(
"Error in sitecustomize; set PYTHONVERBOSE for traceback:\n"
"%s: %s\n" %
(err.__class__.__name__, err))
def execusercustomize():
"""Run custom user specific code, if available."""
try:
try:
import usercustomize # noqa: F401
except ImportError as exc:
if exc.name == 'usercustomize':
pass
else:
raise
except Exception as err:
if sys.flags.verbose:
sys.excepthook(*sys.exc_info())
else:
sys.stderr.write(
"Error in usercustomize; set PYTHONVERBOSE for traceback:\n"
"%s: %s\n" %
(err.__class__.__name__, err))
def main():
"""Add standard site-specific directories to the module search path.
This function is called automatically when this module is imported,
unless the python interpreter was started with the -S flag.
"""
global ENABLE_USER_SITE
orig_path = sys.path[:]
removeduppaths()
if orig_path != sys.path:
# removeduppaths() might make sys.path absolute.
# Fix __file__ of already imported modules too.
abs_paths()
state = StartupState(set())
_venv(state)
if ENABLE_USER_SITE is None:
ENABLE_USER_SITE = check_enableusersite()
state.addusersitepackages()
state.addsitepackages()
# PEP 829: flush accumulated data from all .pth and .start files.
# Paths are extended first, then deprecated import lines are exec'd,
# and finally .start entry points are executed — ensuring sys.path is
# fully populated before any startup code runs.
state.process()
setquit()
setcopyright()
sethelper()
if not sys.flags.isolated:
enablerlcompleter()
execsitecustomize()
if ENABLE_USER_SITE:
execusercustomize()
# Prevent extending of sys.path when python was started with -S and
# site is imported later.
if not sys.flags.no_site:
main()
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 directory is disabled by user
2 - user site directory is disabled by super user
or for security reasons
>2 - unknown error
"""
args = sys.argv[1:]
if not args:
user_base = getuserbase()
user_site = getusersitepackages()
print("sys.path = [")
for dir in sys.path:
print(" %r," % (dir,))
print("]")
def exists(path):
if path is not None and os.path.isdir(path):
return "exists"
else:
return "doesn't exist"
print(f"USER_BASE: {user_base!r} ({exists(user_base)})")
print(f"USER_SITE: {user_site!r} ({exists(user_site)})")
print(f"ENABLE_USER_SITE: {ENABLE_USER_SITE!r}")
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__':
_script()