Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
self-hosted-runner:
# Pending release of actionlint > 1.7.11 for macos-26-intel support
# https://github.com/rhysd/actionlint/pull/629
labels: ["macos-26-intel"]

config-variables: null

paths:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/add-issue-header.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
# Only ever run once
- opened

permissions: {}

jobs:
add-header:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ on:
- 'main'
- '3.*'

permissions:
contents: read
permissions: {}

concurrency:
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/jit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ on:
paths: *paths
workflow_dispatch:

permissions:
contents: read
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand Down Expand Up @@ -99,9 +98,9 @@ jobs:
- false
include:
- target: x86_64-apple-darwin/clang
runner: macos-26-intel
runner: macos-15-intel
- target: aarch64-apple-darwin/clang
runner: macos-26
runner: macos-15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ name: Lint

on: [push, pull_request, workflow_dispatch]

permissions:
contents: read
permissions: {}

env:
FORCE_COLOR: 1
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ on:
- "Tools/requirements-dev.txt"
workflow_dispatch:

permissions:
contents: read
permissions: {}

env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/new-bugs-announce-notifier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ on:
types:
- opened

permissions:
issues: read
permissions: {}

jobs:
notify-new-bugs-announce:
runs-on: ubuntu-latest
permissions:
issues: read
timeout-minutes: 10
steps:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/require-pr-label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]

permissions: {}

jobs:
label-dnm:
name: DO-NOT-MERGE
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
schedule:
- cron: "0 */6 * * *"

permissions: {}

jobs:
stale:
if: github.repository_owner == 'python'
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/tail-call.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ on:
paths: *paths
workflow_dispatch:

permissions:
contents: read
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand All @@ -32,9 +31,9 @@ jobs:
matrix:
include:
- target: x86_64-apple-darwin/clang
runner: macos-26-intel
runner: macos-15-intel
- target: aarch64-apple-darwin/clang
runner: macos-26
runner: macos-15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/verify-ensurepip-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ on:
- '.github/workflows/verify-ensurepip-wheels.yml'
- 'Tools/build/verify_ensurepip_wheels.py'

permissions:
contents: read
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/verify-expat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ on:
- 'Modules/expat/**'
- '.github/workflows/verify-expat.yml'

permissions:
contents: read
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand Down
6 changes: 6 additions & 0 deletions Doc/library/subprocess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,12 @@ functions.
the value in ``pw_uid`` will be used. If the value is an integer, it will
be passed verbatim. (POSIX only)

.. note::

Specifying *user* will not drop existing supplementary group memberships!
The caller must also pass ``extra_groups=()`` to reduce the group membership
of the child process for security purposes.

.. availability:: POSIX
.. versionadded:: 3.9

Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_interpframe.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ static inline void _PyFrame_Copy(_PyInterpreterFrame *src, _PyInterpreterFrame *
int stacktop = (int)(src->stackpointer - src->localsplus);
assert(stacktop >= 0);
dest->stackpointer = dest->localsplus + stacktop;
// visited is GC bookkeeping for the current stack walk, not frame state.
dest->visited = 0;
#ifdef Py_DEBUG
dest->lltrace = src->lltrace;
#endif
for (int i = 0; i < stacktop; i++) {
dest->localsplus[i] = PyStackRef_MakeHeapSafe(src->localsplus[i]);
}
Expand Down
119 changes: 104 additions & 15 deletions Lib/_pyrepl/_module_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import importlib
import os
import pkgutil
import re
import sys
import token
import tokenize
Expand All @@ -16,17 +17,31 @@
TYPE_CHECKING = False

if TYPE_CHECKING:
from types import ModuleType
from typing import Any, Iterable, Iterator, Mapping
from .types import CompletionAction


HARDCODED_SUBMODULES = {
# Standard library submodules that are not detected by pkgutil.iter_modules
# but can be imported, so should be proposed in completion
"collections": ["abc"],
"math": ["integer"],
"os": ["path"],
"xml.parsers.expat": ["errors", "model"],
}

AUTO_IMPORT_DENYLIST = {
# Standard library modules/submodules that have import side effects
# and must not be automatically imported to complete attributes
re.compile(r"antigravity"), # Calls webbrowser.open
re.compile(r"idlelib\..+"), # May open IDLE GUI
re.compile(r"test\..+"), # Various side-effects
re.compile(r"this"), # Prints to stdout
re.compile(r"_ios_support"), # Spawns a subprocess
re.compile(r".+\.__main__"), # Should not be imported
}


def make_default_module_completer() -> ModuleCompleter:
# Inside pyrepl, __package__ is set to None by default
Expand All @@ -52,11 +67,17 @@ class ModuleCompleter:
def __init__(self, namespace: Mapping[str, Any] | None = None) -> None:
self.namespace = namespace or {}
self._global_cache: list[pkgutil.ModuleInfo] = []
self._failed_imports: set[str] = set()
self._curr_sys_path: list[str] = sys.path[:]
self._stdlib_path = os.path.dirname(importlib.__path__[0])

def get_completions(self, line: str) -> list[str] | None:
"""Return the next possible import completions for 'line'."""
def get_completions(self, line: str) -> tuple[list[str], CompletionAction | None] | None:
"""Return the next possible import completions for 'line'.

For attributes completion, if the module to complete from is not
imported, also return an action (prompt + callback to run if the
user press TAB again) to import the module.
"""
result = ImportParser(line).parse()
if not result:
return None
Expand All @@ -65,24 +86,26 @@ def get_completions(self, line: str) -> list[str] | None:
except Exception:
# Some unexpected error occurred, make it look like
# no completions are available
return []
return [], None

def complete(self, from_name: str | None, name: str | None) -> list[str]:
def complete(self, from_name: str | None, name: str | None) -> tuple[list[str], CompletionAction | None]:
if from_name is None:
# import x.y.z<tab>
assert name is not None
path, prefix = self.get_path_and_prefix(name)
modules = self.find_modules(path, prefix)
return [self.format_completion(path, module) for module in modules]
return [self.format_completion(path, module) for module in modules], None

if name is None:
# from x.y.z<tab>
path, prefix = self.get_path_and_prefix(from_name)
modules = self.find_modules(path, prefix)
return [self.format_completion(path, module) for module in modules]
return [self.format_completion(path, module) for module in modules], None

# from x.y import z<tab>
return self.find_modules(from_name, name)
submodules = self.find_modules(from_name, name)
attributes, action = self.find_attributes(from_name, name)
return sorted({*submodules, *attributes}), action

def find_modules(self, path: str, prefix: str) -> list[str]:
"""Find all modules under 'path' that start with 'prefix'."""
Expand All @@ -100,23 +123,25 @@ def _find_modules(self, path: str, prefix: str) -> list[str]:
if self.is_suggestion_match(module.name, prefix)]
return sorted(builtin_modules + third_party_modules)

if path.startswith('.'):
# Convert relative path to absolute path
package = self.namespace.get('__package__', '')
path = self.resolve_relative_name(path, package) # type: ignore[assignment]
if path is None:
return []
path = self._resolve_relative_path(path) # type: ignore[assignment]
if path is None:
return []

modules: Iterable[pkgutil.ModuleInfo] = self.global_cache
imported_module = sys.modules.get(path.split('.')[0])
if imported_module:
# Filter modules to those who name and specs match the
# Filter modules to those whose name and specs match the
# imported module to avoid invalid suggestions
spec = imported_module.__spec__
if spec:
def _safe_find_spec(mod: pkgutil.ModuleInfo) -> bool:
try:
return mod.module_finder.find_spec(mod.name, None) == spec
except Exception:
return False
modules = [mod for mod in modules
if mod.name == spec.name
and mod.module_finder.find_spec(mod.name, None) == spec]
and _safe_find_spec(mod)]
else:
modules = []

Expand All @@ -141,6 +166,32 @@ def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool:
return (isinstance(module_info.module_finder, FileFinder)
and module_info.module_finder.path == self._stdlib_path)

def find_attributes(self, path: str, prefix: str) -> tuple[list[str], CompletionAction | None]:
"""Find all attributes of module 'path' that start with 'prefix'."""
attributes, action = self._find_attributes(path, prefix)
# Filter out invalid attribute names
# (for example those containing dashes that cannot be imported with 'import')
return [attr for attr in attributes if attr.isidentifier()], action

def _find_attributes(self, path: str, prefix: str) -> tuple[list[str], CompletionAction | None]:
path = self._resolve_relative_path(path) # type: ignore[assignment]
if path is None:
return [], None

imported_module = sys.modules.get(path)
if not imported_module:
if path in self._failed_imports: # Do not propose to import again
return [], None
imported_module = self._maybe_import_module(path)
if not imported_module:
return [], self._get_import_completion_action(path)
try:
module_attributes = dir(imported_module)
except Exception:
module_attributes = []
return [attr_name for attr_name in module_attributes
if self.is_suggestion_match(attr_name, prefix)], None

def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
if prefix:
return module_name.startswith(prefix)
Expand Down Expand Up @@ -185,6 +236,13 @@ def format_completion(self, path: str, module: str) -> str:
return f'{path}{module}'
return f'{path}.{module}'

def _resolve_relative_path(self, path: str) -> str | None:
"""Resolve a relative import path to absolute. Returns None if unresolvable."""
if path.startswith('.'):
package = self.namespace.get('__package__', '')
return self.resolve_relative_name(path, package)
return path

def resolve_relative_name(self, name: str, package: str) -> str | None:
"""Resolve a relative module name to an absolute name.

Expand All @@ -209,8 +267,39 @@ def global_cache(self) -> list[pkgutil.ModuleInfo]:
if not self._global_cache or self._curr_sys_path != sys.path:
self._curr_sys_path = sys.path[:]
self._global_cache = list(pkgutil.iter_modules())
self._failed_imports.clear() # retry on sys.path change
return self._global_cache

def _maybe_import_module(self, fqname: str) -> ModuleType | None:
if any(pattern.fullmatch(fqname) for pattern in AUTO_IMPORT_DENYLIST):
# Special-cased modules with known import side-effects
return None
root = fqname.split(".")[0]
mod_info = next((m for m in self.global_cache if m.name == root), None)
if not mod_info or not self._is_stdlib_module(mod_info):
# Only import stdlib modules (no risk of import side-effects)
return None
try:
return importlib.import_module(fqname)
except Exception:
sys.modules.pop(fqname, None) # Clean half-imported module
return None

def _get_import_completion_action(self, path: str) -> CompletionAction:
prompt = ("[ module not imported, press again to import it "
"and propose attributes ]")

def _do_import() -> str | None:
try:
importlib.import_module(path)
return None
except Exception as exc:
sys.modules.pop(path, None) # Clean half-imported module
self._failed_imports.add(path)
return f"[ error during import: {exc} ]"

return (prompt, _do_import)


class ImportParser:
"""
Expand Down
Loading
Loading