mirror of
https://github.com/python/cpython.git
synced 2026-06-05 01:10:53 +00:00
* 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>
1148 lines
41 KiB
Python
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()
|