Compare commits

..

5 Commits

65 changed files with 420 additions and 3195 deletions

View File

@@ -1,60 +0,0 @@
---
name: rustpython-capi-expansion
description: Implement missing RustPython C-API functions in crates/capi using the pyo3-ffi header split mapping (`pyo3-ffi/src/*.rs`, mirroring CPython C API headers). Use this whenever the user asks to add or port C-API functions (for example from setobject.h, dictobject.h, unicodeobject.h) or add capi tests.
---
# RustPython C-API Expansion
Use this workflow for adding missing C-API functions to RustPython.
## Source of truth for target files
- Use this mapping source: `pyo3-ffi/src/*.rs`, which mirrors the CPython header split used by the C API.
- Map requested header APIs to `crates/capi/src/<header_basename>.rs` using that split. Examples:
- `setobject.h` -> `crates/capi/src/setobject.rs`
- `dictobject.h` -> `crates/capi/src/dictobject.rs`
- `unicodeobject.h` -> `crates/capi/src/unicodeobject.rs`
- Do not invent alternate target modules when the header split implies a direct target.
- If the target file is not present yet, create it and wire it in `crates/capi/src/lib.rs`.
## Implementation workflow
1. Identify requested missing APIs from the user request and their originating C API header.
2. Open nearby capi modules in `crates/capi/src/` and follow existing style and patterns.
3. Implement only the requested functions in the mapped target file.
4. Keep behavior aligned with CPython C-API contracts.
5. Prefer using existing `rustpython-vm` functionality as much as possible instead of re-implementing behavior in capi.
6. If a needed `rustpython-vm` helper exists but is private, make it public with a minimal, focused visibility change.
7. Prefer direct contract assumptions over defensive null checks unless required by the established local style.
8. Add basic tests only; do not overfit with very specific edge-case clutter.
9. In tests, use `pyo3` only as a safe wrapper over the API. Avoid raw pointer-heavy direct FFI-style tests.
10. Run tests from `crates/capi`.
## Testing rules
- Run test commands with working directory set to `crates/capi`.
- Prefer targeted tests first (module/function filter), then broader capi tests if needed.
- Keep test names concise (no required `test_` prefix).
## Style rules
- Follow existing RustPython capi coding style in neighboring files.
- Reuse `rustpython-vm` methods and types first; avoid duplicating VM logic in capi wrappers.
- When exposing previously private VM helpers, keep the API surface minimal and avoid unrelated refactors.
- Only expose and implement ABI-stable C-API surface needed for `abi3` / `abi3t`.
- Add comments only when they explain non-obvious behavior.
- Keep edits minimal and focused on requested API expansion.
## Completion checklist
- [ ] All requested functions implemented in mapped target file.
- [ ] New module exported in `crates/capi/src/lib.rs` when applicable.
- [ ] Basic safe-wrapper `pyo3` tests added/updated.
- [ ] Tests executed from `crates/capi` and passing for changed area.
- [ ] Final response includes changed file paths and test command summary.
## Example prompts this skill should handle
- "Implement these missing functions from `dictobject.h`."
- "Add `setobject.h` C-API functions in RustPython and include basic tests."
- "Port the listed `unicodeobject.h` APIs in capi, follow existing style, and run tests from `crates/capi`."

View File

@@ -33,6 +33,7 @@ env:
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_RELEASE_DEBUG: 0
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # TODO: Remove on 2026/06/02
CI: true
jobs:
@@ -309,7 +310,11 @@ jobs:
extra_test_args: [] # TODO: Enable '-u all'
env_polluting_tests:
- test_set
skips: []
skips:
- test_rlcompleter
- test_pathlib # panic by surrogate chars
- test_posixpath # OSError: (22, 'The filename, directory name, or volume label syntax is incorrect. (os error 123)')
- test_venv # couple of failing tests
timeout: 50
fail-fast: false
steps:
@@ -540,6 +545,12 @@ jobs:
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
path: ~/.cache/prek
# TODO: Remove on 2026/06/02 when node24 is the default
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
package-manager-cache: false
node-version: "24"
- name: install prek
id: prek
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4

View File

@@ -15,6 +15,7 @@ on:
env:
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls-aws-lc,jit,host_env
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' # TODO: Remove on 2026/06/02
jobs:
# codecov collects code coverage data from the rust tests, python snippets and python test suite.
@@ -24,8 +25,6 @@ jobs:
runs-on: ubuntu-latest
# Disable this scheduled job when running on a fork.
if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }}
env:
INSTA_WORKSPACE_ROOT: ${{ github.workspace }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -57,7 +56,7 @@ jobs:
- name: Upload to Codecov
if: ${{ github.event_name != 'pull_request' }}
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./codecov.lcov

2
Cargo.lock generated
View File

@@ -3479,7 +3479,6 @@ name = "rustpython-capi"
version = "0.5.0"
dependencies = [
"bitflags 2.11.1",
"itertools 0.14.0",
"num-complex",
"pyo3",
"rustpython-stdlib",
@@ -3740,6 +3739,7 @@ dependencies = [
"num_enum",
"optional",
"rustpython-wtf8",
"unic-ucd-category",
]
[[package]]

View File

@@ -86,27 +86,22 @@ class REPLThread(threading.Thread):
global return_code
try:
if not sys.flags.quiet:
banner = (
f'asyncio REPL {sys.version} on {sys.platform}\n'
f'Use "await" directly instead of "asyncio.run()".\n'
f'Type "help", "copyright", "credits" or "license" '
f'for more information.\n'
)
banner = (
f'asyncio REPL {sys.version} on {sys.platform}\n'
f'Use "await" directly instead of "asyncio.run()".\n'
f'Type "help", "copyright", "credits" or "license" '
f'for more information.\n'
)
console.write(banner)
console.write(banner)
if not sys.flags.isolated and (startup_path := os.getenv("PYTHONSTARTUP")):
if startup_path := os.getenv("PYTHONSTARTUP"):
sys.audit("cpython.run_startup", startup_path)
try:
import tokenize
with tokenize.open(startup_path) as f:
startup_code = compile(f.read(), startup_path, "exec")
import tokenize
with tokenize.open(startup_path) as f:
startup_code = compile(f.read(), startup_path, "exec")
exec(startup_code, console.locals)
except SystemExit:
raise
except BaseException:
console.showtraceback()
ps1 = getattr(sys, "ps1", ">>> ")
if CAN_USE_PYREPL:
@@ -241,5 +236,4 @@ if __name__ == '__main__':
break
console.write('exiting asyncio REPL...\n')
loop.close()
sys.exit(return_code)

View File

@@ -1345,17 +1345,6 @@ class BaseEventLoop(events.AbstractEventLoop):
# have a chance to get called before "ssl_protocol.connection_made()".
transport.pause_reading()
# gh-142352: move buffered StreamReader data to SSLProtocol
if server_side:
from .streams import StreamReaderProtocol
if isinstance(protocol, StreamReaderProtocol):
stream_reader = getattr(protocol, '_stream_reader', None)
if stream_reader is not None:
buffer = stream_reader._buffer
if buffer:
ssl_protocol._incoming.write(buffer)
buffer.clear()
transport.set_protocol(ssl_protocol)
conmade_cb = self.call_soon(ssl_protocol.connection_made, transport)
resume_cb = self.call_soon(transport.resume_reading)

View File

@@ -265,7 +265,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
# to avoid hanging forever in self._wait as otherwise _exit_waiters
# would never be woken up, we wake them up here.
for waiter in self._exit_waiters:
if not waiter.done():
if not waiter.cancelled():
waiter.set_result(self._returncode)
if all(p is not None and p.disconnected
for p in self._pipes.values()):
@@ -278,7 +278,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
finally:
# wake up futures waiting for wait()
for waiter in self._exit_waiters:
if not waiter.done():
if not waiter.cancelled():
waiter.set_result(self._returncode)
self._exit_waiters = None
self._loop = None

View File

@@ -392,7 +392,7 @@ def _chain_future(source, destination):
def _call_check_cancel(destination):
if destination.cancelled():
if source_loop is None or source_loop is events._get_running_loop():
if source_loop is None or source_loop is dest_loop:
source.cancel()
else:
source_loop.call_soon_threadsafe(source.cancel)
@@ -401,7 +401,7 @@ def _chain_future(source, destination):
if (destination.cancelled() and
dest_loop is not None and dest_loop.is_closed()):
return
if dest_loop is None or dest_loop is events._get_running_loop():
if dest_loop is None or dest_loop is source_loop:
_set_state(destination, source)
else:
if dest_loop.is_closed():

View File

@@ -37,7 +37,7 @@ class Queue(mixins._LoopBoundMixin):
is an integer greater than 0, then "await put()" will block when the
queue reaches maxsize, until an item is removed by get().
Unlike queue.Queue, you can reliably know this Queue's size
Unlike the standard library Queue, you can reliably know this Queue's size
with qsize(), since your single-threaded asyncio application won't be
interrupted between calling qsize() and doing an operation on the Queue.
"""

View File

@@ -10,6 +10,7 @@ import itertools
import msvcrt
import os
import subprocess
import tempfile
import warnings
@@ -23,7 +24,6 @@ BUFSIZE = 8192
PIPE = subprocess.PIPE
STDOUT = subprocess.STDOUT
_mmap_counter = itertools.count()
_MAX_PIPE_ATTEMPTS = 20
# Replacement for os.pipe() using handles instead of fds
@@ -31,6 +31,10 @@ _MAX_PIPE_ATTEMPTS = 20
def pipe(*, duplex=False, overlapped=(True, True), bufsize=BUFSIZE):
"""Like os.pipe() but with overlapped support and using handles not fds."""
address = tempfile.mktemp(
prefix=r'\\.\pipe\python-pipe-{:d}-{:d}-'.format(
os.getpid(), next(_mmap_counter)))
if duplex:
openmode = _winapi.PIPE_ACCESS_DUPLEX
access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE
@@ -52,20 +56,9 @@ def pipe(*, duplex=False, overlapped=(True, True), bufsize=BUFSIZE):
h1 = h2 = None
try:
for attempts in itertools.count():
address = r'\\.\pipe\python-pipe-{:d}-{:d}-{}'.format(
os.getpid(), next(_mmap_counter), os.urandom(8).hex())
try:
h1 = _winapi.CreateNamedPipe(
address, openmode, _winapi.PIPE_WAIT,
1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL)
break
except OSError as e:
if attempts >= _MAX_PIPE_ATTEMPTS:
raise
if e.winerror not in (_winapi.ERROR_PIPE_BUSY,
_winapi.ERROR_ACCESS_DENIED):
raise
h1 = _winapi.CreateNamedPipe(
address, openmode, _winapi.PIPE_WAIT,
1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL)
h2 = _winapi.CreateFile(
address, access, 0, _winapi.NULL, _winapi.OPEN_EXISTING,
@@ -111,9 +104,8 @@ class PipeHandle:
def close(self, *, CloseHandle=_winapi.CloseHandle):
if self._handle is not None:
handle = self._handle
CloseHandle(self._handle)
self._handle = None
CloseHandle(handle)
def __del__(self, _warn=warnings.warn):
if self._handle is not None:

View File

@@ -1375,14 +1375,6 @@ def _find_and_load(name, import_):
# NOTE: because of this, initializing must be set *before*
# putting the new module in sys.modules.
_lock_unlock_module(name)
else:
# Verify the module is still in sys.modules. Another thread may have
# removed it (due to import failure) between our sys.modules.get()
# above and the _initializing check. If removed, we retry the import
# to preserve normal semantics: the caller gets the exception from
# the actual import failure rather than a synthetic error.
if sys.modules.get(name) is not module:
return _find_and_load(name, import_)
if module is None:
message = f'import of {name} halted; None in sys.modules'

View File

@@ -946,7 +946,7 @@ class FileLoader:
def get_data(self, path):
"""Return the data from path as raw bytes."""
if isinstance(self, (SourceLoader, SourcelessFileLoader, ExtensionFileLoader)):
if isinstance(self, (SourceLoader, ExtensionFileLoader)):
with _io.open_code(str(path)) as file:
return file.read()
else:

View File

@@ -11,12 +11,13 @@ __all__ = [ 'Client', 'Listener', 'Pipe', 'wait' ]
import errno
import io
import itertools
import os
import sys
import socket
import struct
import time
import tempfile
import itertools
from . import util
@@ -38,14 +39,11 @@ except ImportError:
#
#
# 64 KiB is the default PIPE buffer size of most POSIX platforms.
BUFSIZE = 64 * 1024
BUFSIZE = 8192
# A very generous timeout when it comes to local connections...
CONNECTION_TIMEOUT = 20.
_mmap_counter = itertools.count()
_MAX_PIPE_ATTEMPTS = 100
default_family = 'AF_INET'
families = ['AF_INET']
@@ -76,14 +74,10 @@ def arbitrary_address(family):
if family == 'AF_INET':
return ('localhost', 0)
elif family == 'AF_UNIX':
# NOTE: util.get_temp_dir() is a 0o700 per-process directory. A
# mktemp-style ToC vs ToU concern is not important; bind() surfaces
# the extremely unlikely collision as EADDRINUSE.
return os.path.join(util.get_temp_dir(),
f'sock-{os.urandom(6).hex()}')
return tempfile.mktemp(prefix='sock-', dir=util.get_temp_dir())
elif family == 'AF_PIPE':
return (r'\\.\pipe\pyc-%d-%d-%s' %
(os.getpid(), next(_mmap_counter), os.urandom(8).hex()))
return tempfile.mktemp(prefix=r'\\.\pipe\pyc-%d-%d-' %
(os.getpid(), next(_mmap_counter)), dir="")
else:
raise ValueError('unrecognized family')
@@ -185,10 +179,6 @@ class _ConnectionBase:
finally:
self._handle = None
def _detach(self):
"""Stop managing the underlying file descriptor or handle."""
self._handle = None
def send_bytes(self, buf, offset=0, size=None):
"""Send the bytes data from a bytes-like object"""
self._check_closed()
@@ -326,32 +316,22 @@ if _winapi:
try:
ov, err = _winapi.ReadFile(self._handle, bsize,
overlapped=True)
sentinel = object()
return_value = sentinel
try:
try:
if err == _winapi.ERROR_IO_PENDING:
waitres = _winapi.WaitForMultipleObjects(
[ov.event], False, INFINITE)
assert waitres == WAIT_OBJECT_0
except:
ov.cancel()
raise
finally:
nread, err = ov.GetOverlappedResult(True)
if err == 0:
f = io.BytesIO()
f.write(ov.getbuffer())
return_value = f
elif err == _winapi.ERROR_MORE_DATA:
return_value = self._get_more_data(ov, maxsize)
if err == _winapi.ERROR_IO_PENDING:
waitres = _winapi.WaitForMultipleObjects(
[ov.event], False, INFINITE)
assert waitres == WAIT_OBJECT_0
except:
if return_value is sentinel:
raise
if return_value is not sentinel:
return return_value
ov.cancel()
raise
finally:
nread, err = ov.GetOverlappedResult(True)
if err == 0:
f = io.BytesIO()
f.write(ov.getbuffer())
return f
elif err == _winapi.ERROR_MORE_DATA:
return self._get_more_data(ov, maxsize)
except OSError as e:
if e.winerror == _winapi.ERROR_BROKEN_PIPE:
raise EOFError
@@ -412,8 +392,7 @@ class Connection(_ConnectionBase):
handle = self._handle
remaining = size
while remaining > 0:
to_read = min(BUFSIZE, remaining)
chunk = read(handle, to_read)
chunk = read(handle, remaining)
n = len(chunk)
if n == 0:
if remaining == size:
@@ -476,29 +455,17 @@ class Listener(object):
def __init__(self, address=None, family=None, backlog=1, authkey=None):
family = family or (address and address_type(address)) \
or default_family
address = address or arbitrary_address(family)
_validate_family(family)
if family == 'AF_PIPE':
self._listener = PipeListener(address, backlog)
else:
self._listener = SocketListener(address, family, backlog)
if authkey is not None and not isinstance(authkey, bytes):
raise TypeError('authkey should be a byte string')
if family == 'AF_PIPE':
if address:
self._listener = PipeListener(address, backlog)
else:
for attempts in itertools.count():
address = arbitrary_address(family)
try:
self._listener = PipeListener(address, backlog)
break
except OSError as e:
if attempts >= _MAX_PIPE_ATTEMPTS:
raise
if e.winerror not in (_winapi.ERROR_PIPE_BUSY,
_winapi.ERROR_ACCESS_DENIED):
raise
else:
address = address or arbitrary_address(family)
self._listener = SocketListener(address, family, backlog)
self._authkey = authkey
def accept(self):
@@ -586,6 +553,7 @@ else:
'''
Returns pair of connection objects at either end of a pipe
'''
address = arbitrary_address('AF_PIPE')
if duplex:
openmode = _winapi.PIPE_ACCESS_DUPLEX
access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE
@@ -595,25 +563,15 @@ else:
access = _winapi.GENERIC_WRITE
obsize, ibsize = 0, BUFSIZE
for attempts in itertools.count():
address = arbitrary_address('AF_PIPE')
try:
h1 = _winapi.CreateNamedPipe(
address, openmode | _winapi.FILE_FLAG_OVERLAPPED |
_winapi.FILE_FLAG_FIRST_PIPE_INSTANCE,
_winapi.PIPE_TYPE_MESSAGE | _winapi.PIPE_READMODE_MESSAGE |
_winapi.PIPE_WAIT,
1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER,
# default security descriptor: the handle cannot be inherited
_winapi.NULL
)
break
except OSError as e:
if attempts >= _MAX_PIPE_ATTEMPTS:
raise
if e.winerror not in (_winapi.ERROR_PIPE_BUSY,
_winapi.ERROR_ACCESS_DENIED):
raise
h1 = _winapi.CreateNamedPipe(
address, openmode | _winapi.FILE_FLAG_OVERLAPPED |
_winapi.FILE_FLAG_FIRST_PIPE_INSTANCE,
_winapi.PIPE_TYPE_MESSAGE | _winapi.PIPE_READMODE_MESSAGE |
_winapi.PIPE_WAIT,
1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER,
# default security descriptor: the handle cannot be inherited
_winapi.NULL
)
h2 = _winapi.CreateFile(
address, access, 0, _winapi.NULL, _winapi.OPEN_EXISTING,
_winapi.FILE_FLAG_OVERLAPPED, _winapi.NULL

View File

@@ -145,13 +145,7 @@ class BaseContext(object):
'''Check whether this is a fake forked process in a frozen executable.
If so then run code specified by commandline and exit.
'''
# gh-140814: allow_none=True avoids locking in the default start
# method, which would cause a later set_start_method() to fail.
# None is safe to pass through: spawn.freeze_support()
# independently detects whether this process is a spawned
# child, so the start method check here is only an optimization.
if (getattr(sys, 'frozen', False)
and self.get_start_method(allow_none=True) in ('spawn', None)):
if self.get_start_method() == 'spawn' and getattr(sys, 'frozen', False):
from .spawn import freeze_support
freeze_support()
@@ -173,7 +167,7 @@ class BaseContext(object):
'''
# This is undocumented. In previous versions of multiprocessing
# its only effect was to make socket objects inheritable on Windows.
from . import connection # noqa: F401
from . import connection
def set_executable(self, executable):
'''Sets the path to a python.exe or pythonw.exe binary used to run
@@ -265,12 +259,13 @@ class DefaultContext(BaseContext):
def get_all_start_methods(self):
"""Returns a list of the supported start methods, default first."""
default = self._default_context.get_start_method()
start_method_names = [default]
start_method_names.extend(
name for name in _concrete_contexts if name != default
)
return start_method_names
if sys.platform == 'win32':
return ['spawn']
else:
methods = ['spawn', 'fork'] if sys.platform == 'darwin' else ['fork', 'spawn']
if reduction.HAVE_SEND_HANDLE:
methods.append('forkserver')
return methods
#
@@ -325,15 +320,14 @@ if sys.platform != 'win32':
'spawn': SpawnContext(),
'forkserver': ForkServerContext(),
}
# bpo-33725: running arbitrary code after fork() is no longer reliable
# on macOS since macOS 10.14 (Mojave). Use spawn by default instead.
# gh-84559: We changed everyones default to a thread safeish one in 3.14.
if reduction.HAVE_SEND_HANDLE and sys.platform != 'darwin':
_default_context = DefaultContext(_concrete_contexts['forkserver'])
else:
if sys.platform == 'darwin':
# bpo-33725: running arbitrary code after fork() is no longer reliable
# on macOS since macOS 10.14 (Mojave). Use spawn by default instead.
_default_context = DefaultContext(_concrete_contexts['spawn'])
else:
_default_context = DefaultContext(_concrete_contexts['fork'])
else: # Windows
else:
class SpawnProcess(process.BaseProcess):
_start_method = 'spawn'

View File

@@ -33,7 +33,7 @@ from queue import Queue
class DummyProcess(threading.Thread):
def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
threading.Thread.__init__(self, group, target, name, args, kwargs)
self._pid = None
self._children = weakref.WeakKeyDictionary()

View File

@@ -9,7 +9,6 @@ import sys
import threading
import warnings
from . import AuthenticationError
from . import connection
from . import process
from .context import reduction
@@ -26,7 +25,6 @@ __all__ = ['ensure_running', 'get_inherited_fds', 'connect_to_new_process',
MAXFDS_TO_SEND = 256
SIGNED_STRUCT = struct.Struct('q') # large enough for pid_t
_AUTHKEY_LEN = 32 # <= PIPEBUF so it fits a single write to an empty pipe.
#
# Forkserver class
@@ -35,7 +33,6 @@ _AUTHKEY_LEN = 32 # <= PIPEBUF so it fits a single write to an empty pipe.
class ForkServer(object):
def __init__(self):
self._forkserver_authkey = None
self._forkserver_address = None
self._forkserver_alive_fd = None
self._forkserver_pid = None
@@ -62,7 +59,6 @@ class ForkServer(object):
if not util.is_abstract_socket_namespace(self._forkserver_address):
os.unlink(self._forkserver_address)
self._forkserver_address = None
self._forkserver_authkey = None
def set_forkserver_preload(self, modules_names):
'''Set list of module names to try to load in forkserver process.'''
@@ -87,7 +83,6 @@ class ForkServer(object):
process data.
'''
self.ensure_running()
assert self._forkserver_authkey
if len(fds) + 4 >= MAXFDS_TO_SEND:
raise ValueError('too many fds')
with socket.socket(socket.AF_UNIX) as client:
@@ -98,18 +93,6 @@ class ForkServer(object):
resource_tracker.getfd()]
allfds += fds
try:
client.setblocking(True)
wrapped_client = connection.Connection(client.fileno())
# The other side of this exchange happens in the child as
# implemented in main().
try:
connection.answer_challenge(
wrapped_client, self._forkserver_authkey)
connection.deliver_challenge(
wrapped_client, self._forkserver_authkey)
finally:
wrapped_client._detach()
del wrapped_client
reduction.sendfds(client, allfds)
return parent_r, parent_w
except:
@@ -137,30 +120,20 @@ class ForkServer(object):
return
# dead, launch it again
os.close(self._forkserver_alive_fd)
self._forkserver_authkey = None
self._forkserver_address = None
self._forkserver_alive_fd = None
self._forkserver_pid = None
# gh-144503: sys_argv is passed as real argv elements after the
# ``-c cmd`` rather than repr'd into main_kws so that a large
# parent sys.argv cannot push the single ``-c`` command string
# over the OS per-argument length limit (MAX_ARG_STRLEN on Linux).
# The child sees them as sys.argv[1:].
cmd = ('import sys; '
'from multiprocessing.forkserver import main; '
'main(%d, %d, %r, sys_argv=sys.argv[1:], **%r)')
cmd = ('from multiprocessing.forkserver import main; ' +
'main(%d, %d, %r, **%r)')
main_kws = {}
sys_argv = None
if self._preload_modules:
data = spawn.get_preparation_data('ignore')
if 'sys_path' in data:
main_kws['sys_path'] = data['sys_path']
if 'init_main_from_path' in data:
main_kws['main_path'] = data['init_main_from_path']
if 'sys_argv' in data:
sys_argv = data['sys_argv']
with socket.socket(socket.AF_UNIX) as listener:
address = connection.arbitrary_address('AF_UNIX')
@@ -172,33 +145,19 @@ class ForkServer(object):
# all client processes own the write end of the "alive" pipe;
# when they all terminate the read end becomes ready.
alive_r, alive_w = os.pipe()
# A short lived pipe to initialize the forkserver authkey.
authkey_r, authkey_w = os.pipe()
try:
fds_to_pass = [listener.fileno(), alive_r, authkey_r]
main_kws['authkey_r'] = authkey_r
fds_to_pass = [listener.fileno(), alive_r]
cmd %= (listener.fileno(), alive_r, self._preload_modules,
main_kws)
exe = spawn.get_executable()
args = [exe] + util._args_from_interpreter_flags()
args += ['-c', cmd]
if sys_argv is not None:
args += sys_argv
pid = util.spawnv_passfds(exe, args, fds_to_pass)
except:
os.close(alive_w)
os.close(authkey_w)
raise
finally:
os.close(alive_r)
os.close(authkey_r)
# Authenticate our control socket to prevent access from
# processes we have not shared this key with.
try:
self._forkserver_authkey = os.urandom(_AUTHKEY_LEN)
os.write(authkey_w, self._forkserver_authkey)
finally:
os.close(authkey_w)
self._forkserver_address = address
self._forkserver_alive_fd = alive_w
self._forkserver_pid = pid
@@ -207,21 +166,9 @@ class ForkServer(object):
#
#
def main(listener_fd, alive_r, preload, main_path=None, sys_path=None,
*, sys_argv=None, authkey_r=None):
"""Run forkserver."""
if authkey_r is not None:
try:
authkey = os.read(authkey_r, _AUTHKEY_LEN)
assert len(authkey) == _AUTHKEY_LEN, f'{len(authkey)} < {_AUTHKEY_LEN}'
finally:
os.close(authkey_r)
else:
authkey = b''
def main(listener_fd, alive_r, preload, main_path=None, sys_path=None):
'''Run forkserver.'''
if preload:
if sys_argv is not None:
sys.argv[:] = sys_argv
if sys_path is not None:
sys.path[:] = sys_path
if '__main__' in preload and main_path is not None:
@@ -315,24 +262,8 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None,
if listener in rfds:
# Incoming fork request
with listener.accept()[0] as s:
try:
if authkey:
wrapped_s = connection.Connection(s.fileno())
# The other side of this exchange happens in
# in connect_to_new_process().
try:
connection.deliver_challenge(
wrapped_s, authkey)
connection.answer_challenge(
wrapped_s, authkey)
finally:
wrapped_s._detach()
del wrapped_s
# Receive fds from client
fds = reduction.recvfds(s, MAXFDS_TO_SEND + 1)
except (EOFError, BrokenPipeError, AuthenticationError):
s.close()
continue
# Receive fds from client
fds = reduction.recvfds(s, MAXFDS_TO_SEND + 1)
if len(fds) > MAXFDS_TO_SEND:
raise RuntimeError(
"Too many ({0:n}) fds to send".format(
@@ -400,14 +331,13 @@ def _serve_one(child_r, fds, unused_fds, handlers):
#
def read_signed(fd):
data = bytearray(SIGNED_STRUCT.size)
unread = memoryview(data)
while unread:
count = os.readinto(fd, unread)
if count == 0:
data = b''
length = SIGNED_STRUCT.size
while len(data) < length:
s = os.read(fd, length - len(data))
if not s:
raise EOFError('unexpected EOF')
unread = unread[count:]
data += s
return SIGNED_STRUCT.unpack(data)[0]
def write_signed(fd, n):

View File

@@ -18,7 +18,6 @@ import sys
import threading
import signal
import array
import collections.abc
import queue
import time
import types
@@ -1059,14 +1058,12 @@ class IteratorProxy(BaseProxy):
class AcquirerProxy(BaseProxy):
_exposed_ = ('acquire', 'release', 'locked')
_exposed_ = ('acquire', 'release')
def acquire(self, blocking=True, timeout=None):
args = (blocking,) if timeout is None else (blocking, timeout)
return self._callmethod('acquire', args)
def release(self):
return self._callmethod('release')
def locked(self):
return self._callmethod('locked')
def __enter__(self):
return self._callmethod('acquire')
def __exit__(self, exc_type, exc_val, exc_tb):
@@ -1074,7 +1071,7 @@ class AcquirerProxy(BaseProxy):
class ConditionProxy(AcquirerProxy):
_exposed_ = ('acquire', 'release', 'locked', 'wait', 'notify', 'notify_all')
_exposed_ = ('acquire', 'release', 'wait', 'notify', 'notify_all')
def wait(self, timeout=None):
return self._callmethod('wait', (timeout,))
def notify(self, n=1):
@@ -1162,10 +1159,10 @@ class ValueProxy(BaseProxy):
BaseListProxy = MakeProxyType('BaseListProxy', (
'__add__', '__contains__', '__delitem__', '__getitem__', '__imul__',
'__len__', '__mul__', '__reversed__', '__rmul__', '__setitem__',
'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop',
'remove', 'reverse', 'sort',
'__add__', '__contains__', '__delitem__', '__getitem__', '__len__',
'__mul__', '__reversed__', '__rmul__', '__setitem__',
'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove',
'reverse', 'sort', '__imul__'
))
class ListProxy(BaseListProxy):
def __iadd__(self, value):
@@ -1177,55 +1174,18 @@ class ListProxy(BaseListProxy):
__class_getitem__ = classmethod(types.GenericAlias)
collections.abc.MutableSequence.register(BaseListProxy)
_BaseDictProxy = MakeProxyType('_BaseDictProxy', (
'__contains__', '__delitem__', '__getitem__', '__ior__', '__iter__',
'__len__', '__or__', '__reversed__', '__ror__',
'__setitem__', 'clear', 'copy', 'fromkeys', 'get', 'items',
_BaseDictProxy = MakeProxyType('DictProxy', (
'__contains__', '__delitem__', '__getitem__', '__iter__', '__len__',
'__setitem__', 'clear', 'copy', 'get', 'items',
'keys', 'pop', 'popitem', 'setdefault', 'update', 'values'
))
_BaseDictProxy._method_to_typeid_ = {
'__iter__': 'Iterator',
}
class DictProxy(_BaseDictProxy):
def __ior__(self, value):
self._callmethod('__ior__', (value,))
return self
__class_getitem__ = classmethod(types.GenericAlias)
collections.abc.MutableMapping.register(_BaseDictProxy)
_BaseSetProxy = MakeProxyType("_BaseSetProxy", (
'__and__', '__class_getitem__', '__contains__', '__iand__', '__ior__',
'__isub__', '__iter__', '__ixor__', '__len__', '__or__', '__rand__',
'__ror__', '__rsub__', '__rxor__', '__sub__', '__xor__',
'__ge__', '__gt__', '__le__', '__lt__',
'add', 'clear', 'copy', 'difference', 'difference_update', 'discard',
'intersection', 'intersection_update', 'isdisjoint', 'issubset',
'issuperset', 'pop', 'remove', 'symmetric_difference',
'symmetric_difference_update', 'union', 'update',
))
class SetProxy(_BaseSetProxy):
def __ior__(self, value):
self._callmethod('__ior__', (value,))
return self
def __iand__(self, value):
self._callmethod('__iand__', (value,))
return self
def __ixor__(self, value):
self._callmethod('__ixor__', (value,))
return self
def __isub__(self, value):
self._callmethod('__isub__', (value,))
return self
__class_getitem__ = classmethod(types.GenericAlias)
collections.abc.MutableMapping.register(_BaseSetProxy)
ArrayProxy = MakeProxyType('ArrayProxy', (
'__len__', '__getitem__', '__setitem__'
@@ -1277,7 +1237,6 @@ SyncManager.register('Barrier', threading.Barrier, BarrierProxy)
SyncManager.register('Pool', pool.Pool, PoolProxy)
SyncManager.register('list', list, ListProxy)
SyncManager.register('dict', dict, DictProxy)
SyncManager.register('set', set, SetProxy)
SyncManager.register('Value', Value, ValueProxy)
SyncManager.register('Array', Array, ArrayProxy)
SyncManager.register('Namespace', Namespace, NamespaceProxy)

View File

@@ -54,9 +54,6 @@ class Popen(object):
if self.wait(timeout=0.1) is None:
raise
def interrupt(self):
self._send_signal(signal.SIGINT)
def terminate(self):
self._send_signal(signal.SIGTERM)
@@ -67,17 +64,7 @@ class Popen(object):
code = 1
parent_r, child_w = os.pipe()
child_r, parent_w = os.pipe()
# gh-146313: Tell the resource tracker's at-fork handler to keep
# the inherited pipe fd so this child reuses the parent's tracker
# (gh-80849) rather than closing it and launching its own.
from .resource_tracker import _fork_intent
_fork_intent.preserve_fd = True
try:
self.pid = os.fork()
finally:
# Reset in both parent and child so the flag does not leak
# into a subsequent raw os.fork() or nested Process launch.
_fork_intent.preserve_fd = False
self.pid = os.fork()
if self.pid == 0:
try:
atexit._clear()

View File

@@ -77,7 +77,7 @@ class BaseProcess(object):
def _Popen(self):
raise NotImplementedError
def __init__(self, group=None, target=None, name=None, args=(), kwargs=None,
def __init__(self, group=None, target=None, name=None, args=(), kwargs={},
*, daemon=None):
assert group is None, 'group argument must be None for now'
count = next(_process_counter)
@@ -89,7 +89,7 @@ class BaseProcess(object):
self._closed = False
self._target = target
self._args = tuple(args)
self._kwargs = dict(kwargs) if kwargs else {}
self._kwargs = dict(kwargs)
self._name = name or type(self).__name__ + '-' + \
':'.join(str(i) for i in self._identity)
if daemon is not None:
@@ -125,13 +125,6 @@ class BaseProcess(object):
del self._target, self._args, self._kwargs
_children.add(self)
def interrupt(self):
'''
Terminate process; sends SIGINT signal
'''
self._check_closed()
self._popen.interrupt()
def terminate(self):
'''
Terminate process; sends SIGTERM signal or uses TerminateProcess()

View File

@@ -121,7 +121,7 @@ class Queue(object):
def qsize(self):
# Raises NotImplementedError on Mac OSX because of broken sem_getvalue()
return self._maxsize - self._sem.get_value()
return self._maxsize - self._sem._semlock._get_value()
def empty(self):
return not self._poll()

View File

@@ -139,12 +139,15 @@ else:
__all__ += ['DupFd', 'sendfds', 'recvfds']
import array
# On MacOSX we should acknowledge receipt of fds -- see Issue14669
ACKNOWLEDGE = sys.platform == 'darwin'
def sendfds(sock, fds):
'''Send an array of fds over an AF_UNIX socket.'''
fds = array.array('i', fds)
msg = bytes([len(fds) % 256])
sock.sendmsg([msg], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)])
if sock.recv(1) != b'A':
if ACKNOWLEDGE and sock.recv(1) != b'A':
raise RuntimeError('did not receive acknowledgement of fd')
def recvfds(sock, size):
@@ -155,11 +158,8 @@ else:
if not msg and not ancdata:
raise EOFError
try:
# We send/recv an Ack byte after the fds to work around an old
# macOS bug; it isn't clear if this is still required but it
# makes unit testing fd sending easier.
# See: https://github.com/python/cpython/issues/58874
sock.send(b'A') # Acknowledge
if ACKNOWLEDGE:
sock.send(b'A')
if len(ancdata) != 1:
raise RuntimeError('received %d items of ancdata' %
len(ancdata))

View File

@@ -20,7 +20,6 @@ import os
import signal
import sys
import threading
import time
import warnings
from collections import deque
@@ -52,8 +51,12 @@ if os.name == 'posix':
# absence of POSIX named semaphores. In that case, no named semaphores were
# ever opened, so no cleanup would be necessary.
if hasattr(_multiprocessing, 'sem_unlink'):
_CLEANUP_FUNCS['semaphore'] = _multiprocessing.sem_unlink
_CLEANUP_FUNCS['shared_memory'] = _posixshmem.shm_unlink
_CLEANUP_FUNCS.update({
'semaphore': _multiprocessing.sem_unlink,
})
_CLEANUP_FUNCS.update({
'shared_memory': _posixshmem.shm_unlink,
})
class ReentrantCallError(RuntimeError):
@@ -76,10 +79,6 @@ class ResourceTracker(object):
# The reader should understand all formats.
self._use_simple_format = True
# Set to True by _stop_locked() if the waitpid polling loop ran to
# its timeout without reaping the tracker. Exposed for tests.
self._waitpid_timed_out = False
def _reentrant_call_error(self):
# gh-109629: this happens if an explicit call to the ResourceTracker
# gets interrupted by a garbage collection, invoking a finalizer (*)
@@ -92,51 +91,16 @@ class ResourceTracker(object):
# making sure child processess are cleaned before ResourceTracker
# gets destructed.
# see https://github.com/python/cpython/issues/88887
# gh-146313: use a timeout to avoid deadlocking if a forked child
# still holds the pipe's write end open.
self._stop(use_blocking_lock=False, wait_timeout=1.0)
self._stop(use_blocking_lock=False)
def _after_fork_in_child(self):
# gh-146313: Called in the child right after os.fork().
#
# The tracker process is a child of the *parent*, not of us, so we
# could never waitpid() it anyway. Clearing _pid means our __del__
# becomes a no-op (the early return for _pid is None).
#
# Whether we keep the inherited _fd depends on who forked us:
#
# - multiprocessing.Process with the 'fork' start method sets
# _fork_intent.preserve_fd before forking. The child keeps the
# fd and reuses the parent's tracker (gh-80849). This is safe
# because multiprocessing's atexit handler joins all children
# before the parent's __del__ runs, so by then the fd copies
# are gone and the parent can reap the tracker promptly.
#
# - A raw os.fork() leaves the flag unset. We close the fd in the child after forking so
# the parent's __del__ can reap the tracker without waiting
# for the child to exit. If we later need a tracker, ensure_running()
# will launch a fresh one.
self._lock._at_fork_reinit()
self._reentrant_messages.clear()
self._pid = None
self._exitcode = None
if (self._fd is not None and
not getattr(_fork_intent, 'preserve_fd', False)):
fd = self._fd
self._fd = None
try:
os.close(fd)
except OSError:
pass
def _stop(self, use_blocking_lock=True, wait_timeout=None):
def _stop(self, use_blocking_lock=True):
if use_blocking_lock:
with self._lock:
self._stop_locked(wait_timeout=wait_timeout)
self._stop_locked()
else:
acquired = self._lock.acquire(blocking=False)
try:
self._stop_locked(wait_timeout=wait_timeout)
self._stop_locked()
finally:
if acquired:
self._lock.release()
@@ -146,10 +110,6 @@ class ResourceTracker(object):
close=os.close,
waitpid=os.waitpid,
waitstatus_to_exitcode=os.waitstatus_to_exitcode,
monotonic=time.monotonic,
sleep=time.sleep,
WNOHANG=getattr(os, 'WNOHANG', None),
wait_timeout=None,
):
# This shouldn't happen (it might when called by a finalizer)
# so we check for it anyway.
@@ -166,30 +126,7 @@ class ResourceTracker(object):
self._fd = None
try:
if wait_timeout is None:
_, status = waitpid(self._pid, 0)
else:
# gh-146313: A forked child may still hold the pipe's write
# end open, preventing the tracker from seeing EOF and
# exiting. Poll with WNOHANG to avoid blocking forever.
deadline = monotonic() + wait_timeout
delay = 0.001
while True:
result_pid, status = waitpid(self._pid, WNOHANG)
if result_pid != 0:
break
remaining = deadline - monotonic()
if remaining <= 0:
# The tracker is still running; it will be
# reparented to PID 1 (or the nearest subreaper)
# when we exit, and reaped there once all pipe
# holders release their fd.
self._pid = None
self._exitcode = None
self._waitpid_timed_out = True
return
delay = min(delay * 2, remaining, 0.1)
sleep(delay)
_, status = waitpid(self._pid, 0)
except ChildProcessError:
self._pid = None
self._exitcode = None
@@ -375,24 +312,12 @@ class ResourceTracker(object):
self._ensure_running_and_write(msg)
# gh-146313: Per-thread flag set by .popen_fork.Popen._launch() just before
# os.fork(), telling _after_fork_in_child() to keep the inherited pipe fd so
# the child can reuse this tracker (gh-80849). Unset for raw os.fork() calls,
# where the child instead closes the fd so the parent's __del__ can reap the
# tracker. Using threading.local() keeps multiple threads calling
# popen_fork.Popen._launch() at once from clobbering eachothers intent.
_fork_intent = threading.local()
_resource_tracker = ResourceTracker()
ensure_running = _resource_tracker.ensure_running
register = _resource_tracker.register
unregister = _resource_tracker.unregister
getfd = _resource_tracker.getfd
# gh-146313: See _after_fork_in_child docstring.
if hasattr(os, 'register_at_fork'):
os.register_at_fork(after_in_child=_resource_tracker._after_fork_in_child)
def _decode_message(line):
if line.startswith(b'{'):

View File

@@ -539,6 +539,6 @@ class ShareableList:
if value == entry:
return position
else:
raise ValueError("ShareableList.index(x): x not in list")
raise ValueError(f"{value!r} not in this container")
__class_getitem__ = classmethod(types.GenericAlias)

View File

@@ -184,7 +184,7 @@ def get_preparation_data(name):
sys_argv=sys.argv,
orig_dir=process.ORIGINAL_DIR,
dir=os.getcwd(),
start_method=get_start_method(allow_none=True),
start_method=get_start_method(),
)
# Figure out whether to initialise main in the subprocess as a module

View File

@@ -21,21 +21,22 @@ from . import context
from . import process
from . import util
# TODO: Do any platforms still lack a functioning sem_open?
# Try to import the mp.synchronize module cleanly, if it fails
# raise ImportError for platforms lacking a working sem_open implementation.
# See issue 3770
try:
from _multiprocessing import SemLock, sem_unlink
except ImportError:
except (ImportError):
raise ImportError("This platform lacks a functioning sem_open" +
" implementation. https://github.com/python/cpython/issues/48020.")
" implementation, therefore, the required" +
" synchronization primitives needed will not" +
" function, see issue 3770.")
#
# Constants
#
# These match the enum in Modules/_multiprocessing/semaphore.c
RECURSIVE_MUTEX = 0
SEMAPHORE = 1
RECURSIVE_MUTEX, SEMAPHORE = list(range(2))
SEM_VALUE_MAX = _multiprocessing.SemLock.SEM_VALUE_MAX
#
@@ -90,9 +91,6 @@ class SemLock(object):
self.acquire = self._semlock.acquire
self.release = self._semlock.release
def locked(self):
return self._semlock._is_zero()
def __enter__(self):
return self._semlock.__enter__()
@@ -135,16 +133,11 @@ class Semaphore(SemLock):
SemLock.__init__(self, SEMAPHORE, value, SEM_VALUE_MAX, ctx=ctx)
def get_value(self):
'''Returns current value of Semaphore.
Raises NotImplementedError on Mac OSX
because of broken sem_getvalue().
'''
return self._semlock._get_value()
def __repr__(self):
try:
value = self.get_value()
value = self._semlock._get_value()
except Exception:
value = 'unknown'
return '<%s(value=%s)>' % (self.__class__.__name__, value)
@@ -160,7 +153,7 @@ class BoundedSemaphore(Semaphore):
def __repr__(self):
try:
value = self.get_value()
value = self._semlock._get_value()
except Exception:
value = 'unknown'
return '<%s(value=%s, maxvalue=%s)>' % \
@@ -252,8 +245,8 @@ class Condition(object):
def __repr__(self):
try:
num_waiters = (self._sleeping_count.get_value() -
self._woken_count.get_value())
num_waiters = (self._sleeping_count._semlock._get_value() -
self._woken_count._semlock._get_value())
except Exception:
num_waiters = 'unknown'
return '<%s(%s, %s)>' % (self.__class__.__name__, self._lock, num_waiters)

View File

@@ -14,12 +14,12 @@ import weakref
import atexit
import threading # we want threading to install it's
# cleanup function before multiprocessing does
from subprocess import _args_from_interpreter_flags # noqa: F401
from subprocess import _args_from_interpreter_flags
from . import process
__all__ = [
'sub_debug', 'debug', 'info', 'sub_warning', 'warn', 'get_logger',
'sub_debug', 'debug', 'info', 'sub_warning', 'get_logger',
'log_to_stderr', 'get_temp_dir', 'register_after_fork',
'is_exiting', 'Finalize', 'ForkAwareThreadLock', 'ForkAwareLocal',
'close_all_fds_except', 'SUBDEBUG', 'SUBWARNING',
@@ -54,7 +54,7 @@ def info(msg, *args):
if _logger:
_logger.log(INFO, msg, *args, stacklevel=2)
def warn(msg, *args):
def _warn(msg, *args):
if _logger:
_logger.log(WARNING, msg, *args, stacklevel=2)
@@ -196,14 +196,14 @@ def _get_base_temp_dir(tempfile):
try:
base_system_tempdir = tempfile._get_default_tempdir(dirlist)
except FileNotFoundError:
warn("Process-wide temporary directory %s will not be usable for "
"creating socket files and no usable system-wide temporary "
"directory was found in %s", base_tempdir, dirlist)
_warn("Process-wide temporary directory %s will not be usable for "
"creating socket files and no usable system-wide temporary "
"directory was found in %s", base_tempdir, dirlist)
# At this point, the system-wide temporary directory is not usable
# but we may assume that the user-defined one is, even if we will
# not be able to write socket files out there.
return base_tempdir
warn("Ignoring user-defined temporary directory: %s", base_tempdir)
_warn("Ignoring user-defined temporary directory: %s", base_tempdir)
# at most max(map(len, dirlist)) + 14 + 14 = 36 characters
assert len(base_system_tempdir) + 14 + 14 < _SUN_PATH_MAX
return base_system_tempdir

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
# gh-144503: Test that the forkserver can start when the parent process has
# a very large sys.argv. Prior to the fix, sys.argv was repr'd into the
# forkserver ``-c`` command string which could exceed the OS limit on the
# length of a single argv element (MAX_ARG_STRLEN on Linux, ~128 KiB),
# causing posix_spawn to fail and the parent to see a BrokenPipeError.
import multiprocessing
import sys
EXPECTED_LEN = 5002 # argv[0] + 5000 padding entries + sentinel
def fun():
print(f"worker:{len(sys.argv)}:{sys.argv[-1]}")
if __name__ == "__main__":
# Inflate sys.argv well past 128 KiB before the forkserver is started.
sys.argv[1:] = ["x" * 50] * 5000 + ["sentinel"]
assert len(sys.argv) == EXPECTED_LEN
ctx = multiprocessing.get_context("forkserver")
p = ctx.Process(target=fun)
p.start()
p.join()
sys.exit(p.exitcode)
else:
# This branch runs when the forkserver preloads this module as
# __mp_main__; confirm the large argv was propagated intact.
print(f"preload:{len(sys.argv)}:{sys.argv[-1]}")

View File

@@ -1,22 +0,0 @@
# gh-143706: Test that sys.argv is correctly set during main module import
# when using forkserver with __main__ preloading.
import multiprocessing
import sys
# This will be printed during module import - sys.argv should be correct here
print(f"module:{sys.argv[1:]}")
def fun():
# This will be printed when the function is called
print(f"fun:{sys.argv[1:]}")
if __name__ == "__main__":
ctx = multiprocessing.get_context("forkserver")
ctx.set_forkserver_preload(['__main__'])
fun()
p = ctx.Process(target=fun)
p.start()
p.join()

View File

@@ -1,4 +1,5 @@
import io
import platform
import queue
import re
import subprocess
@@ -16,11 +17,17 @@ from unittest.mock import patch
if sys.platform != "android":
raise unittest.SkipTest("Android-specific")
api_level = platform.android_ver().api_level
# (name, level, fileno)
STREAM_INFO = [("stdout", "I", 1), ("stderr", "W", 2)]
# Test redirection of stdout and stderr to the Android log.
@unittest.skipIf(
api_level < 23 and platform.machine() == "aarch64",
"SELinux blocks reading logs on older ARM64 emulators"
)
class TestAndroidOutput(unittest.TestCase):
maxDiff = None
@@ -35,41 +42,31 @@ class TestAndroidOutput(unittest.TestCase):
for line in self.logcat_process.stdout:
self.logcat_queue.put(line.rstrip("\n"))
self.logcat_process.stdout.close()
self.logcat_thread = Thread(target=logcat_thread)
self.logcat_thread.start()
try:
from ctypes import CDLL, c_char_p, c_int
android_log_write = getattr(CDLL("liblog.so"), "__android_log_write")
android_log_write.argtypes = (c_int, c_char_p, c_char_p)
ANDROID_LOG_INFO = 4
from ctypes import CDLL, c_char_p, c_int
android_log_write = getattr(CDLL("liblog.so"), "__android_log_write")
android_log_write.argtypes = (c_int, c_char_p, c_char_p)
ANDROID_LOG_INFO = 4
# Separate tests using a marker line with a different tag.
tag, message = "python.test", f"{self.id()} {time()}"
android_log_write(
ANDROID_LOG_INFO, tag.encode("UTF-8"), message.encode("UTF-8"))
self.assert_log("I", tag, message, skip=True)
except:
# If setUp throws an exception, tearDown is not automatically
# called. Avoid leaving a dangling thread which would keep the
# Python process alive indefinitely.
self.tearDown()
raise
# Separate tests using a marker line with a different tag.
tag, message = "python.test", f"{self.id()} {time()}"
android_log_write(
ANDROID_LOG_INFO, tag.encode("UTF-8"), message.encode("UTF-8"))
self.assert_log("I", tag, message, skip=True, timeout=5)
def assert_logs(self, level, tag, expected, **kwargs):
for line in expected:
self.assert_log(level, tag, line, **kwargs)
def assert_log(self, level, tag, expected, *, skip=False):
deadline = time() + LOOPBACK_TIMEOUT
def assert_log(self, level, tag, expected, *, skip=False, timeout=0.5):
deadline = time() + timeout
while True:
try:
line = self.logcat_queue.get(timeout=(deadline - time()))
except queue.Empty:
raise self.failureException(
f"line not found: {expected!r}"
) from None
self.fail(f"line not found: {expected!r}")
if match := re.fullmatch(fr"(.)/{tag}: (.*)", line):
try:
self.assertEqual(level, match[1])
@@ -84,42 +81,35 @@ class TestAndroidOutput(unittest.TestCase):
self.logcat_process.wait(LOOPBACK_TIMEOUT)
self.logcat_thread.join(LOOPBACK_TIMEOUT)
# Avoid an irrelevant warning about threading._dangling.
self.logcat_thread = None
@contextmanager
def reconfigure(self, stream, **settings):
original_settings = {key: getattr(stream, key, None) for key in settings.keys()}
stream.reconfigure(**settings)
def unbuffered(self, stream):
stream.reconfigure(write_through=True)
try:
yield
finally:
stream.reconfigure(**original_settings)
stream.reconfigure(write_through=False)
# In --verbose3 mode, sys.stdout and sys.stderr are captured, so we can't
# test them directly. Detect this mode and use some temporary streams with
# the same properties.
def stream_context(self, stream_name, level):
# https://developer.android.com/ndk/reference/group/logging
prio = {"I": 4, "W": 5}[level]
stack = ExitStack()
stack.enter_context(self.subTest(stream_name))
# In --verbose3 mode, sys.stdout and sys.stderr are captured, so we can't
# test them directly. Detect this mode and use some temporary streams with
# the same properties.
stream = getattr(sys, stream_name)
native_stream = getattr(sys, f"__{stream_name}__")
if isinstance(stream, io.StringIO):
# https://developer.android.com/ndk/reference/group/logging
prio = {"I": 4, "W": 5}[level]
stack.enter_context(
patch(
f"sys.{stream_name}",
stream := TextLogStream(
prio, f"python.{stream_name}", native_stream,
TextLogStream(
prio, f"python.{stream_name}", native_stream.fileno(),
errors="backslashreplace"
),
)
)
# The tests assume the stream is initially buffered.
stack.enter_context(self.reconfigure(stream, write_through=False))
return stack
def test_str(self):
@@ -146,7 +136,7 @@ class TestAndroidOutput(unittest.TestCase):
self.assert_logs(level, tag, lines)
# Single-line messages,
with self.reconfigure(stream, write_through=True):
with self.unbuffered(stream):
write("", [])
write("a")
@@ -176,18 +166,14 @@ class TestAndroidOutput(unittest.TestCase):
# Multi-line messages. Avoid identical consecutive lines, as
# they may activate "chatty" filtering and break the tests.
#
# Additional spaces will appear in the output where necessary to
# protect leading newlines.
write("\nx", [" "])
write("\nx", [""])
write("\na\n", ["x", "a"])
write("\n", [" "])
write("\n\n", [" ", " "])
write("\n", [""])
write("b\n", ["b"])
write("c\n\n", ["c", " "])
write("c\n\n", ["c", ""])
write("d\ne", ["d"])
write("xx", [])
write("f\n\ng", ["exxf", " "])
write("f\n\ng", ["exxf", ""])
write("\n", ["g"])
# Since this is a line-based logging system, line buffering
@@ -197,17 +183,16 @@ class TestAndroidOutput(unittest.TestCase):
# However, buffering can be turned off completely if you want a
# flush after every write.
with self.reconfigure(stream, write_through=True):
write("\nx", [" ", "x"])
write("\na\n", [" ", "a"])
write("\n", [" "])
write("\n\n", [" ", " "])
with self.unbuffered(stream):
write("\nx", ["", "x"])
write("\na\n", ["", "a"])
write("\n", [""])
write("b\n", ["b"])
write("c\n\n", ["c", " "])
write("c\n\n", ["c", ""])
write("d\ne", ["d", "e"])
write("xx", ["xx"])
write("f\n\ng", ["f", " ", "g"])
write("\n", [" "])
write("f\n\ng", ["f", "", "g"])
write("\n", [""])
# "\r\n" should be translated into "\n".
write("hello\r\n", ["hello"])
@@ -327,16 +312,19 @@ class TestAndroidOutput(unittest.TestCase):
# currently use `logcat -v tag`, which shows each line as if it
# was a separate log entry, but strips a single trailing
# newline.
write(b"\nx", [" ", "x"])
write(b"\na\n", [" ", "a"])
write(b"\n", [" "])
write(b"\n\n", [" ", ""])
#
# On newer versions of Android, all three of the above tools (or
# maybe Logcat itself) will also strip any number of leading
# newlines.
write(b"\nx", ["", "x"] if api_level < 30 else ["x"])
write(b"\na\n", ["", "a"] if api_level < 30 else ["a"])
write(b"\n", [""])
write(b"b\n", ["b"])
write(b"c\n\n", ["c", ""])
write(b"d\ne", ["d", "e"])
write(b"xx", ["xx"])
write(b"f\n\ng", ["f", "", "g"])
write(b"\n", [" "])
write(b"\n", [""])
# "\r\n" should be translated into "\n".
write(b"hello\r\n", ["hello"])

View File

@@ -427,27 +427,6 @@ class BaseSockTestsMixin:
self.loop.run_until_complete(
self._basetest_datagram_recvfrom_into(server_address))
async def _basetest_datagram_recvfrom_into_wrong_size(self, server_address):
# Call sock_sendto() with a size larger than the buffer
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.setblocking(False)
buf = bytearray(5000)
data = b'\x01' * 4096
wrong_size = len(buf) + 1
await self.loop.sock_sendto(sock, data, server_address)
with self.assertRaises(ValueError):
await self.loop.sock_recvfrom_into(
sock, buf, wrong_size)
size, addr = await self.loop.sock_recvfrom_into(sock, buf)
self.assertEqual(buf[:size], data)
def test_recvfrom_into_wrong_size(self):
with test_utils.run_udp_echo_server() as server_address:
self.loop.run_until_complete(
self._basetest_datagram_recvfrom_into_wrong_size(server_address))
async def _basetest_datagram_sendto_blocking(self, server_address):
# Sad path, sock.sendto() raises BlockingIOError
# This involves patching sock.sendto() to raise BlockingIOError but
@@ -663,10 +642,6 @@ if sys.platform == 'win32':
self._basetest_datagram_send_to_non_listening_address(
recvfrom_into))
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; AssertionError: ValueError not raised")
def test_recvfrom_into_wrong_size(self):
return super().test_recvfrom_into_wrong_size()
else:
import selectors

View File

@@ -27,7 +27,6 @@ from test.test_asyncio import utils as test_utils
MACOS = (sys.platform == 'darwin')
BUF_MULTIPLIER = 1024 if not MACOS else 64
HANDSHAKE_TIMEOUT = support.LONG_TIMEOUT
def tearDownModule():
@@ -258,12 +257,15 @@ class TestSSL(test_utils.TestCase):
await fut
async def start_server():
extras = {}
extras = dict(ssl_handshake_timeout=support.SHORT_TIMEOUT)
srv = await asyncio.start_server(
handle_client,
'127.0.0.1', 0,
family=socket.AF_INET,
ssl=sslctx,
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
**extras)
try:
srv_socks = srv.sockets
@@ -320,11 +322,14 @@ class TestSSL(test_utils.TestCase):
sock.close()
async def client(addr):
extras = {}
extras = dict(ssl_handshake_timeout=support.SHORT_TIMEOUT)
reader, writer = await asyncio.open_connection(
*addr,
ssl=client_sslctx,
server_hostname='',
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
**extras)
writer.write(A_DATA)
self.assertEqual(await reader.readexactly(2), b'OK')
@@ -344,8 +349,7 @@ class TestSSL(test_utils.TestCase):
reader, writer = await asyncio.open_connection(
sock=sock,
ssl=client_sslctx,
server_hostname='',
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
server_hostname='')
writer.write(A_DATA)
self.assertEqual(await reader.readexactly(2), b'OK')
@@ -444,7 +448,7 @@ class TestSSL(test_utils.TestCase):
*addr,
ssl=client_sslctx,
server_hostname='',
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
ssl_handshake_timeout=support.SHORT_TIMEOUT)
writer.close()
await self.wait_closed(writer)
@@ -606,7 +610,7 @@ class TestSSL(test_utils.TestCase):
extras = {}
if server_ssl:
extras = dict(ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
extras = dict(ssl_handshake_timeout=support.SHORT_TIMEOUT)
f = loop.create_task(
loop.connect_accepted_socket(
@@ -655,8 +659,7 @@ class TestSSL(test_utils.TestCase):
reader, writer = await asyncio.open_connection(
*addr,
ssl=client_sslctx,
server_hostname='',
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
server_hostname='')
self.assertEqual(await reader.readline(), b'A\n')
writer.write(b'B')
@@ -1150,11 +1153,14 @@ class TestSSL(test_utils.TestCase):
await fut
async def start_server():
extras = {}
srv = await self.loop.create_server(
server_protocol_factory,
'127.0.0.1', 0,
family=socket.AF_INET,
ssl=sslctx_1)
ssl=sslctx_1,
**extras)
try:
srv_socks = srv.sockets
@@ -1204,11 +1210,14 @@ class TestSSL(test_utils.TestCase):
sock.close()
async def client(addr):
extras = {}
extras = dict(ssl_handshake_timeout=support.SHORT_TIMEOUT)
reader, writer = await asyncio.open_connection(
*addr,
ssl=client_sslctx,
server_hostname='',
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
**extras)
writer.write(A_DATA)
self.assertEqual(await reader.readexactly(2), b'OK')
@@ -1278,8 +1287,7 @@ class TestSSL(test_utils.TestCase):
reader, writer = await asyncio.open_connection(
*addr,
ssl=client_sslctx,
server_hostname='',
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
server_hostname='')
sslprotocol = writer.transport._ssl_protocol
writer.write(b'ping')
data = await reader.readexactly(4)
@@ -1391,8 +1399,7 @@ class TestSSL(test_utils.TestCase):
reader, writer = await asyncio.open_connection(
*addr,
ssl=client_sslctx,
server_hostname='',
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
server_hostname='')
writer.write(b'ping')
data = await reader.readexactly(4)
self.assertEqual(data, b'pong')
@@ -1523,8 +1530,7 @@ class TestSSL(test_utils.TestCase):
reader, writer = await asyncio.open_connection(
*addr,
ssl=client_sslctx,
server_hostname='',
ssl_handshake_timeout=HANDSHAKE_TIMEOUT)
server_hostname='')
writer.write(b'ping')
data = await reader.readexactly(4)
self.assertEqual(data, b'pong')

View File

@@ -819,48 +819,6 @@ class StreamTests(test_utils.TestCase):
self.assertEqual(msg1, b"hello world 1!\n")
self.assertEqual(msg2, b"hello world 2!\n")
@unittest.skipIf(ssl is None, 'No ssl module')
def test_start_tls_buffered_data(self):
# gh-142352: test start_tls() with buffered data
async def server_handler(client_reader, client_writer):
# Wait for TLS ClientHello to be buffered before start_tls().
await client_reader._wait_for_data('test_start_tls_buffered_data'),
self.assertTrue(client_reader._buffer)
await client_writer.start_tls(test_utils.simple_server_sslcontext())
line = await client_reader.readline()
self.assertEqual(line, b"ping\n")
client_writer.write(b"pong\n")
await client_writer.drain()
client_writer.close()
await client_writer.wait_closed()
async def client(addr):
reader, writer = await asyncio.open_connection(*addr)
await writer.start_tls(test_utils.simple_client_sslcontext())
writer.write(b"ping\n")
await writer.drain()
line = await reader.readline()
self.assertEqual(line, b"pong\n")
writer.close()
await writer.wait_closed()
async def run_test():
server = await asyncio.start_server(
server_handler, socket_helper.HOSTv4, 0)
server_addr = server.sockets[0].getsockname()
await client(server_addr)
server.close()
await server.wait_closed()
messages = []
self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx))
self.loop.run_until_complete(run_test())
self.assertEqual(messages, [])
def test_streamreader_constructor_without_loop(self):
with self.assertRaisesRegex(RuntimeError, 'no current event loop'):
asyncio.StreamReader()

View File

@@ -111,37 +111,6 @@ class SubprocessTransportTests(test_utils.TestCase):
)
transport.close()
def test_proc_exited_no_invalid_state_error_on_exit_waiters(self):
# gh-145541: when _connect_pipes hasn't completed (so
# _pipes_connected is False) and the process exits, _try_finish()
# sets the result on exit waiters. Then _call_connection_lost() must
# not call set_result() again on the same waiters.
self.loop.set_exception_handler(
lambda loop, context: self.fail(
f"unexpected exception: {context}")
)
waiter = self.loop.create_future()
transport, protocol = self.create_transport(waiter)
# Simulate a waiter registered via _wait() before the process exits.
exit_waiter = self.loop.create_future()
transport._exit_waiters.append(exit_waiter)
# _connect_pipes hasn't completed, so _pipes_connected is False.
self.assertFalse(transport._pipes_connected)
# Simulate process exit. _try_finish() will set the result on
# exit_waiter because _pipes_connected is False, and then schedule
# _call_connection_lost() because _pipes is empty (vacuously all
# disconnected). _call_connection_lost() must skip exit_waiter
# because it's already done.
transport._process_exited(6)
self.loop.run_until_complete(waiter)
self.assertEqual(exit_waiter.result(), 6)
transport.close()
class SubprocessMixin:

View File

@@ -2947,6 +2947,7 @@ class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest,
return super().test_log_destroyed_pending_task()
@unittest.skipUnless(hasattr(futures, '_CFuture') and
hasattr(tasks, '_CTask'),
'requires the C _asyncio module')
@@ -3029,6 +3030,7 @@ class PyTask_PyFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
all_tasks = staticmethod(tasks._py_all_tasks)
current_task = staticmethod(tasks._py_current_task)
@unittest.skipUnless(hasattr(tasks, '_CTask'),
'requires the C _asyncio module')
class CTask_Future_Tests(test_utils.TestCase):
@@ -3061,26 +3063,6 @@ class BaseTaskIntrospectionTests:
_enter_task = None
_leave_task = None
all_tasks = None
Task = None
def test_register_task_resurrection(self):
register_task = self._register_task
class EvilLoop:
def get_debug(self):
return False
def call_exception_handler(self, context):
register_task(context["task"])
async def coro_fn ():
pass
coro = coro_fn()
self.addCleanup(coro.close)
loop = EvilLoop()
with self.assertRaises(AttributeError):
self.Task(coro, loop=loop)
def test__register_task_1(self):
class TaskLike:
@@ -3211,7 +3193,6 @@ class PyIntrospectionTests(test_utils.TestCase, BaseTaskIntrospectionTests):
_leave_task = staticmethod(tasks._py_leave_task)
all_tasks = staticmethod(tasks._py_all_tasks)
current_task = staticmethod(tasks._py_current_task)
Task = tasks._PyTask
@unittest.skipUnless(hasattr(tasks, '_c_register_task'),
@@ -3224,12 +3205,10 @@ class CIntrospectionTests(test_utils.TestCase, BaseTaskIntrospectionTests):
_leave_task = staticmethod(tasks._c_leave_task)
all_tasks = staticmethod(tasks._c_all_tasks)
current_task = staticmethod(tasks._c_current_task)
Task = tasks._CTask
else:
_register_task = _unregister_task = _enter_task = _leave_task = None
class BaseCurrentLoopTests:
current_task = None
@@ -3719,30 +3698,6 @@ class RunCoroutineThreadsafeTests(test_utils.TestCase):
(loop, context), kwargs = callback.call_args
self.assertEqual(context['exception'], exc_context.exception)
def test_run_coroutine_threadsafe_and_cancel(self):
task = None
thread_future = None
# Use a custom task factory to capture the created Task
def task_factory(loop, coro):
nonlocal task
task = asyncio.Task(coro, loop=loop)
return task
self.addCleanup(self.loop.set_task_factory,
self.loop.get_task_factory())
async def target():
nonlocal thread_future
self.loop.set_task_factory(task_factory)
thread_future = asyncio.run_coroutine_threadsafe(asyncio.sleep(10), self.loop)
await asyncio.sleep(0)
thread_future.cancel()
self.loop.run_until_complete(target())
self.assertTrue(task.cancelled())
self.assertTrue(thread_future.cancelled())
class SleepTests(test_utils.TestCase):
def setUp(self):

View File

@@ -77,30 +77,6 @@ class PipeTests(unittest.TestCase):
else:
raise RuntimeError('expected ERROR_INVALID_HANDLE')
def test_pipe_handle_close_after_external_close(self):
# gh-149388: PipeHandle.close() must clear ``_handle`` before calling
# CloseHandle so that if CloseHandle raises on a stale handle the
# PipeHandle is still marked closed and __del__ / subsequent close()
# calls are silent no-ops.
h1, h2 = windows_utils.pipe(overlapped=(False, False))
try:
p = windows_utils.PipeHandle(h1)
# Simulate an external close of the underlying handle (e.g.
# a finalizer race or a concurrent close on the same object).
_winapi.CloseHandle(p.handle)
# First close() still propagates the OSError from CloseHandle,
# but must clear ``_handle`` first.
with self.assertRaises(OSError):
p.close()
self.assertIsNone(p.handle)
# Second close() is a no-op.
p.close()
# __del__ through GC is also a silent no-op — no unraisable.
del p
support.gc_collect()
finally:
_winapi.CloseHandle(h2)
class PopenTests(unittest.TestCase):
@@ -153,25 +129,5 @@ class PopenTests(unittest.TestCase):
pass
class OverlappedRefleakTests(unittest.TestCase):
def test_wsasendto_failure(self):
ov = _overlapped.Overlapped()
buf = bytearray(4096)
with self.assertRaises(OSError):
ov.WSASendTo(0x1234, buf, 0, ("127.0.0.1", 1))
def test_wsarecvfrom_failure(self):
ov = _overlapped.Overlapped()
with self.assertRaises(OSError):
ov.WSARecvFrom(0x1234, 1024, 0)
def test_wsarecvfrominto_failure(self):
ov = _overlapped.Overlapped()
buf = bytearray(4096)
with self.assertRaises(OSError):
ov.WSARecvFromInto(0x1234, buf, len(buf), 0)
if __name__ == '__main__':
unittest.main()

View File

@@ -222,7 +222,6 @@ class BinASCIITest(unittest.TestCase):
assertInvalidLength(b'a' * (4 * 87 + 1))
assertInvalidLength(b'A\tB\nC ??DE') # only 5 valid characters
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Error not raised by a2b_uu
def test_uu(self):
MAX_UU = 45
for backtick in (True, False):
@@ -243,10 +242,6 @@ class BinASCIITest(unittest.TestCase):
self.assertEqual(binascii.a2b_uu(b"\xff"), b"\x00"*31)
self.assertRaises(binascii.Error, binascii.a2b_uu, b"\xff\x00")
self.assertRaises(binascii.Error, binascii.a2b_uu, b"!!!!")
self.assertRaises(binascii.Error, binascii.a2b_uu,
self.type2test(b""))
self.assertRaises(binascii.Error, binascii.a2b_uu,
self.type2test(b"#86)C")[:0])
self.assertRaises(binascii.Error, binascii.b2a_uu, 46*b"!")
# Issue #7701 (crash on a pydebug build)
@@ -445,7 +440,6 @@ class BinASCIITest(unittest.TestCase):
self.assertConversion(binary, converted, restored,
quotetabs=quotetabs, istext=istext, header=header)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Error not raised by a2b_uu
def test_empty_string(self):
# A test for SF bug #1022953. Make sure SystemError is not raised.
empty = self.type2test(b'')
@@ -455,9 +449,6 @@ class BinASCIITest(unittest.TestCase):
binascii.crc_hqx(empty, 0)
continue
f = getattr(binascii, func)
if func == 'a2b_uu':
self.assertRaises(binascii.Error, f, empty)
continue
try:
f(empty)
except Exception as err:

View File

@@ -293,27 +293,6 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple'])
def test_builtin_call_async_genexpr_no_crash(self):
async def f_all():
return all(await 2 for _ in [])
async def f_any():
return any(await 2 for _ in [])
async def f_tuple():
return tuple(await 2 for _ in [])
async def f_list():
return list(await 2 for _ in [])
async def f_set():
return set(await 2 for _ in [])
for f in (f_all, f_any, f_tuple, f_list, f_set):
with self.subTest(func=f.__name__):
with self.assertRaises(TypeError):
run_yielding_async_fn(f)
def test_ascii(self):
self.assertEqual(ascii(''), '\'\'')
self.assertEqual(ascii(0), '0')
@@ -627,7 +606,7 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
exec(co, glob)
self.assertEqual(type(glob['ticker']()), AsyncGeneratorType)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <_ast.Name object at 0xb40000731e3d1360> is not an instance of <class '_ast.Constant'>
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <_ast.Name object at 0xb40000731e3d1360> is not an instance of <class '_ast.Constant'>
def test_compile_ast(self):
args = ("a*__debug__", "f.py", "exec")
raw = compile(*args, flags = ast.PyCF_ONLY_AST).body[0]
@@ -991,7 +970,7 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
self.assertRaisesRegex(NameError, "name 'superglobal' is not defined",
eval, code, ns)
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_exec_builtins_mapping_import(self):
code = compile("import foo.bar", "test", "exec")
ns = {'__builtins__': types.MappingProxyType({})}
@@ -1000,7 +979,7 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
exec(code, ns)
self.assertEqual(ns['foo'], ('foo.bar', ns, ns, None, 0))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: AttributeError not raised by eval
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: AttributeError not raised by eval
def test_eval_builtins_mapping_reduce(self):
# list_iterator.__reduce__() calls _PyEval_GetBuiltin("iter")
code = compile("x.__reduce__()", "test", "eval")

View File

@@ -204,20 +204,5 @@ class TestDefaultDict(unittest.TestCase):
self.assertEqual(test_dict[key], 2)
self.assertEqual(count, 2)
def test_repr_recursive_factory(self):
# gh-145492: defaultdict.__repr__ should not cause infinite recursion
# when the factory's __repr__ calls repr() on the defaultdict.
class ProblematicFactory:
def __call__(self):
return {}
def __repr__(self):
repr(dd)
return f"ProblematicFactory for {dd}"
dd = defaultdict(ProblematicFactory())
# Should not raise RecursionError
r = repr(dd)
self.assertIn("ProblematicFactory for", r)
if __name__ == "__main__":
unittest.main()

View File

@@ -5,6 +5,7 @@ machinery = util.import_importlib('importlib.machinery')
from test.support import captured_stdout, import_helper, STDLIB_DIR
import contextlib
import os.path
import sys
import types
import unittest
import warnings
@@ -75,7 +76,7 @@ class ExecModuleTests(abc.LoaderTests):
self.assertHasAttr(module, '__spec__')
self.assertEqual(module.__spec__.loader_state.origname, name)
@unittest.skipIf(__import__("sys").platform == "win32", "TODO: RUSTPYTHON")
@unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON")
def test_package(self):
name = '__phello__'
module, output = self.exec_module(name)
@@ -146,7 +147,7 @@ class InspectLoaderTests:
result = self.machinery.FrozenImporter.get_source('__hello__')
self.assertIsNone(result)
@unittest.skipIf(__import__("sys").platform == "win32", "TODO: RUSTPYTHON")
@unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON")
def test_is_package(self):
# Should be able to tell what is a package.
test_for = (('__hello__', False), ('__phello__', True),

View File

@@ -1,3 +1,4 @@
import os
import unittest
from . import util

View File

@@ -155,8 +155,8 @@ class LifetimeTests:
# TODO: RUSTPYTHON; dead weakref module locks not cleaned up in frozen bootstrap
Frozen_LifetimeTests.test_all_locks = unittest.skip("TODO: RUSTPYTHON")(
Frozen_LifetimeTests.test_all_locks
)
Frozen_LifetimeTests.test_all_locks)
def setUpModule():
thread_info = threading_helper.threading_setup()

View File

@@ -263,72 +263,6 @@ class ThreadedImportTests(unittest.TestCase):
'partial', 'pool_in_threads.py')
script_helper.assert_python_ok(fn)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_import_failure_race_condition(self):
# Regression test for race condition where a thread could receive
# a partially-initialized module when another thread's import fails.
# The race occurs when:
# 1. Thread 1 starts importing, adds module to sys.modules
# 2. Thread 2 sees the module in sys.modules
# 3. Thread 1's import fails, removes module from sys.modules
# 4. Thread 2 should NOT return the stale module reference
os.mkdir(TESTFN)
self.addCleanup(shutil.rmtree, TESTFN)
sys.path.insert(0, TESTFN)
self.addCleanup(sys.path.remove, TESTFN)
# Create a module that partially initializes then fails
modname = 'failing_import_module'
with open(os.path.join(TESTFN, modname + '.py'), 'w') as f:
f.write('''
import time
PARTIAL_ATTR = 'initialized'
time.sleep(0.05) # Widen race window
raise RuntimeError("Intentional import failure")
''')
self.addCleanup(forget, modname)
importlib.invalidate_caches()
errors = []
results = []
def do_import(delay=0):
time.sleep(delay)
try:
mod = __import__(modname)
# If we got a module, verify it's in sys.modules
if modname not in sys.modules:
errors.append(
f"Got module {mod!r} but {modname!r} not in sys.modules"
)
elif sys.modules[modname] is not mod:
errors.append(
f"Got different module than sys.modules[{modname!r}]"
)
else:
results.append(('success', mod))
except RuntimeError:
results.append(('RuntimeError',))
except Exception as e:
errors.append(f"Unexpected exception: {e}")
# Run multiple iterations to increase chance of hitting the race
for _ in range(10):
errors.clear()
results.clear()
if modname in sys.modules:
del sys.modules[modname]
t1 = threading.Thread(target=do_import, args=(0,))
t2 = threading.Thread(target=do_import, args=(0.01,))
t1.start()
t2.start()
t1.join()
t2.join()
# Neither thread should have errors about stale modules
self.assertEqual(errors, [], f"Race condition detected: {errors}")
def setUpModule():
thread_info = threading_helper.threading_setup()

View File

@@ -15,10 +15,10 @@ import sys
import tempfile
import types
try: # TODO: RUSTPYTHON
import_helper.import_module("_testmultiphase")
try:
_testsinglephase = import_helper.import_module("_testsinglephase")
except unittest.SkipTest:
_testmultiphase = None
_testsinglephase = None # TODO: RUSTPYTHON
BUILTINS = types.SimpleNamespace()

385
Lib/test/test_mmap.py vendored
View File

@@ -1,12 +1,9 @@
from test.support import (
requires, _2G, _4G, gc_collect, cpython_only, is_emscripten, is_apple,
in_systemd_nspawn_sync_suppressed,
requires, _2G, _4G, gc_collect, cpython_only, is_emscripten
)
from test.support.import_helper import import_module
from test.support.os_helper import TESTFN, unlink
from test.support.script_helper import assert_python_ok
import unittest
import errno
import os
import re
import itertools
@@ -14,7 +11,6 @@ import random
import socket
import string
import sys
import textwrap
import weakref
# Skip test if we can't import mmap.
@@ -36,10 +32,6 @@ if is_emscripten:
class MmapTests(unittest.TestCase):
def setUp(self):
# TODO: RUSTPYTHON; Remove this once windows doesn't get errored on setup:/
if os.name == "nt":
raise unittest.SkipTest("TODO: RUSTPYTHON; Error during class setUp")
if os.path.exists(TESTFN):
os.unlink(TESTFN)
@@ -49,7 +41,6 @@ class MmapTests(unittest.TestCase):
except OSError:
pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'mmap' object has no attribute 'seekable'
def test_basic(self):
# Test mmap module on Unix systems and Windows
@@ -102,12 +93,11 @@ class MmapTests(unittest.TestCase):
self.assertEqual(end, PAGESIZE + 6)
# test seeking around (try to overflow the seek implementation)
self.assertTrue(m.seekable())
self.assertEqual(m.seek(0, 0), 0)
m.seek(0,0)
self.assertEqual(m.tell(), 0)
self.assertEqual(m.seek(42, 1), 42)
m.seek(42,1)
self.assertEqual(m.tell(), 42)
self.assertEqual(m.seek(0, 2), len(m))
m.seek(0,2)
self.assertEqual(m.tell(), len(m))
# Try to seek to negative position...
@@ -172,7 +162,7 @@ class MmapTests(unittest.TestCase):
# Ensuring that readonly mmap can't be write() to
try:
m.seek(0, 0)
m.seek(0,0)
m.write(b'abc')
except TypeError:
pass
@@ -181,7 +171,7 @@ class MmapTests(unittest.TestCase):
# Ensuring that readonly mmap can't be write_byte() to
try:
m.seek(0, 0)
m.seek(0,0)
m.write_byte(b'd')
except TypeError:
pass
@@ -265,79 +255,15 @@ class MmapTests(unittest.TestCase):
# Try writing with PROT_EXEC and without PROT_WRITE
prot = mmap.PROT_READ | getattr(mmap, 'PROT_EXEC', 0)
with open(TESTFN, "r+b") as f:
try:
m = mmap.mmap(f.fileno(), mapsize, prot=prot)
except PermissionError:
# on macOS 14, PROT_READ | PROT_EXEC is not allowed
pass
else:
self.assertRaises(TypeError, m.write, b"abcdef")
self.assertRaises(TypeError, m.write_byte, 0)
m.close()
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
def test_trackfd_parameter(self):
size = 64
with open(TESTFN, "wb") as f:
f.write(b"a"*size)
for close_original_fd in True, False:
with self.subTest(close_original_fd=close_original_fd):
with open(TESTFN, "r+b") as f:
with mmap.mmap(f.fileno(), size, trackfd=False) as m:
if close_original_fd:
f.close()
self.assertEqual(len(m), size)
with self.assertRaises(OSError) as err_cm:
m.size()
self.assertEqual(err_cm.exception.errno, errno.EBADF)
with self.assertRaises(ValueError):
m.resize(size * 2)
with self.assertRaises(ValueError):
m.resize(size // 2)
self.assertEqual(m.closed, False)
# Smoke-test other API
m.write_byte(ord('X'))
m[2] = ord('Y')
m.flush()
with open(TESTFN, "rb") as f:
self.assertEqual(f.read(4), b'XaYa')
self.assertEqual(m.tell(), 1)
m.seek(0)
self.assertEqual(m.tell(), 0)
self.assertEqual(m.read_byte(), ord('X'))
self.assertEqual(m.closed, True)
self.assertEqual(os.stat(TESTFN).st_size, size)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
def test_trackfd_neg1(self):
size = 64
with mmap.mmap(-1, size, trackfd=False) as m:
with self.assertRaises(OSError):
m.size()
with self.assertRaises(ValueError):
m.resize(size // 2)
self.assertEqual(len(m), size)
m[0] = ord('a')
assert m[0] == ord('a')
@unittest.skipIf(os.name != 'nt', 'trackfd only fails on Windows')
def test_no_trackfd_parameter_on_windows(self):
# 'trackffd' is an invalid keyword argument for this function
size = 64
with self.assertRaises(TypeError):
mmap.mmap(-1, size, trackfd=True)
with self.assertRaises(TypeError):
mmap.mmap(-1, size, trackfd=False)
m = mmap.mmap(f.fileno(), mapsize, prot=prot)
self.assertRaises(TypeError, m.write, b"abcdef")
self.assertRaises(TypeError, m.write_byte, 0)
m.close()
def test_bad_file_desc(self):
# Try opening a bad file descriptor...
self.assertRaises(OSError, mmap.mmap, -2, 4096)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_tougher_find(self):
# Do a tougher .find() test. SF bug 515943 pointed out that, in 2.2,
# searching for data with embedded \0 bytes didn't work.
@@ -356,7 +282,6 @@ class MmapTests(unittest.TestCase):
self.assertEqual(m.find(slice + b'x'), -1)
m.close()
@unittest.skip("TODO: RUSTPYTHON; panic")
def test_find_end(self):
# test the new 'end' parameter works as expected
with open(TESTFN, 'wb+') as f:
@@ -374,29 +299,7 @@ class MmapTests(unittest.TestCase):
self.assertEqual(m.find(b'one', 1, -2), -1)
self.assertEqual(m.find(bytearray(b'one')), 0)
for i in range(-n-1, n+1):
for j in range(-n-1, n+1):
for p in [b"o", b"on", b"two", b"ones", b"s"]:
expected = data.find(p, i, j)
self.assertEqual(m.find(p, i, j), expected, (p, i, j))
def test_find_does_not_access_beyond_buffer(self):
try:
flags = mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS
PAGESIZE = mmap.PAGESIZE
PROT_NONE = 0
PROT_READ = mmap.PROT_READ
except AttributeError as e:
raise unittest.SkipTest("mmap flags unavailable") from e
for i in range(0, 2049):
with mmap.mmap(-1, PAGESIZE * (i + 1),
flags=flags, prot=PROT_NONE) as guard:
with mmap.mmap(-1, PAGESIZE * (i + 2048),
flags=flags, prot=PROT_READ) as fm:
fm.find(b"fo", -2)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_rfind(self):
# test the new 'end' parameter works as expected
with open(TESTFN, 'wb+') as f:
@@ -457,7 +360,6 @@ class MmapTests(unittest.TestCase):
self.assertRaises(ValueError, mmap.mmap, f.fileno(), 0,
offset=2147418112)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_move(self):
# make move works everywhere (64-bit format problem earlier)
with open(TESTFN, 'wb+') as f:
@@ -505,6 +407,7 @@ class MmapTests(unittest.TestCase):
m.move(0, 0, 1)
m.move(0, 0, 0)
def test_anonymous(self):
# anonymous mmap.mmap(-1, PAGE)
m = mmap.mmap(-1, PAGESIZE)
@@ -517,7 +420,6 @@ class MmapTests(unittest.TestCase):
m[x] = b
self.assertEqual(m[x], b)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_read_all(self):
m = mmap.mmap(-1, 16)
self.addCleanup(m.close)
@@ -539,7 +441,6 @@ class MmapTests(unittest.TestCase):
m.seek(9)
self.assertEqual(m.read(-42), bytes(range(9, 16)))
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_read_invalid_arg(self):
m = mmap.mmap(-1, 16)
self.addCleanup(m.close)
@@ -656,7 +557,6 @@ class MmapTests(unittest.TestCase):
except OSError:
pass
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_subclass(self):
class anon_mmap(mmap.mmap):
def __new__(klass, *args, **kwargs):
@@ -709,7 +609,6 @@ class MmapTests(unittest.TestCase):
self.assertEqual(m[:], b"012barbaz9")
self.assertRaises(ValueError, m.write, b"ba")
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_non_ascii_byte(self):
for b in (129, 200, 255): # > 128
m = mmap.mmap(-1, 1)
@@ -719,7 +618,6 @@ class MmapTests(unittest.TestCase):
self.assertEqual(m.read_byte(), b)
m.close()
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_tagname(self):
data1 = b"0123456789"
@@ -748,16 +646,14 @@ class MmapTests(unittest.TestCase):
m2.close()
m1.close()
with self.assertRaisesRegex(TypeError, 'tagname'):
mmap.mmap(-1, 8, tagname=1)
@cpython_only
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_sizeof(self):
m1 = mmap.mmap(-1, 100)
tagname = random_tagname()
m2 = mmap.mmap(-1, 100, tagname=tagname)
self.assertGreater(sys.getsizeof(m2), sys.getsizeof(m1))
self.assertEqual(sys.getsizeof(m2),
sys.getsizeof(m1) + len(tagname) + 1)
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_crasher_on_windows(self):
@@ -812,7 +708,6 @@ class MmapTests(unittest.TestCase):
"wrong exception raised in context manager")
self.assertTrue(m.closed, "context manager failed")
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_weakref(self):
# Check mmap objects are weakrefable
mm = mmap.mmap(-1, 16)
@@ -822,7 +717,6 @@ class MmapTests(unittest.TestCase):
gc_collect()
self.assertIs(wr(), None)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_write_returning_the_number_of_bytes_written(self):
mm = mmap.mmap(-1, 16)
self.assertEqual(mm.write(b""), 0)
@@ -830,7 +724,6 @@ class MmapTests(unittest.TestCase):
self.assertEqual(mm.write(b"yz"), 2)
self.assertEqual(mm.write(b"python"), 6)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_resize_past_pos(self):
m = mmap.mmap(-1, 8192)
self.addCleanup(m.close)
@@ -851,7 +744,7 @@ class MmapTests(unittest.TestCase):
with self.assertRaises(TypeError):
m * 2
@unittest.skipIf(sys.platform.startswith("linux"), "TODO: RUSTPYTHON; memmap2 doesn't throw OSError when offset is not a multiple of mmap.PAGESIZE on Linux")
@unittest.skipIf(sys.platform.startswith("linux"), "TODO: RUSTPYTHON, memmap2 doesn't throw OSError when offset is not a multiple of mmap.PAGESIZE on Linux")
def test_flush_return_value(self):
# mm.flush() should return None on success, raise an
# exception on error under all platforms.
@@ -860,13 +753,11 @@ class MmapTests(unittest.TestCase):
mm.write(b'python')
result = mm.flush()
self.assertIsNone(result)
if (sys.platform.startswith(('linux', 'android'))
and not in_systemd_nspawn_sync_suppressed()):
if sys.platform.startswith('linux'):
# 'offset' must be a multiple of mmap.PAGESIZE on Linux.
# See bpo-34754 for details.
self.assertRaises(OSError, mm.flush, 1, len(b'python'))
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_repr(self):
open_mmap_repr_pat = re.compile(
r"<mmap.mmap closed=False, "
@@ -903,16 +794,11 @@ class MmapTests(unittest.TestCase):
match = closed_mmap_repr_pat.match(repr(mm))
self.assertIsNotNone(match)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.skipUnless(hasattr(mmap.mmap, 'madvise'), 'needs madvise')
def test_madvise(self):
size = 2 * PAGESIZE
m = mmap.mmap(-1, size)
class Number:
def __index__(self):
return 2
with self.assertRaisesRegex(ValueError, "madvise start out of bounds"):
m.madvise(mmap.MADV_NORMAL, size)
with self.assertRaisesRegex(ValueError, "madvise start out of bounds"):
@@ -921,84 +807,42 @@ class MmapTests(unittest.TestCase):
m.madvise(mmap.MADV_NORMAL, 0, -1)
with self.assertRaisesRegex(OverflowError, "madvise length too large"):
m.madvise(mmap.MADV_NORMAL, PAGESIZE, sys.maxsize)
with self.assertRaisesRegex(
TypeError, "'str' object cannot be interpreted as an integer"):
m.madvise(mmap.MADV_NORMAL, PAGESIZE, "Not a Number")
self.assertEqual(m.madvise(mmap.MADV_NORMAL), None)
self.assertEqual(m.madvise(mmap.MADV_NORMAL, PAGESIZE), None)
self.assertEqual(m.madvise(mmap.MADV_NORMAL, PAGESIZE, size), None)
self.assertEqual(m.madvise(mmap.MADV_NORMAL, 0, 2), None)
self.assertEqual(m.madvise(mmap.MADV_NORMAL, 0, Number()), None)
self.assertEqual(m.madvise(mmap.MADV_NORMAL, 0, size), None)
@unittest.expectedFailureIf(sys.platform in ("linux", "win32"), "TODO: RUSTPYTHON")
def test_resize_up_anonymous_mapping(self):
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_resize_up_when_mapped_to_pagefile(self):
"""If the mmap is backed by the pagefile ensure a resize up can happen
and that the original data is still in place
"""
start_size = PAGESIZE
new_size = 2 * start_size
data = random.randbytes(start_size)
data = bytes(random.getrandbits(8) for _ in range(start_size))
with mmap.mmap(-1, start_size) as m:
m[:] = data
if sys.platform.startswith(('linux', 'android')):
# Can't expand a shared anonymous mapping on Linux.
# See https://bugzilla.kernel.org/show_bug.cgi?id=8691
with self.assertRaises(ValueError):
m.resize(new_size)
else:
try:
m.resize(new_size)
except SystemError:
pass
else:
self.assertEqual(len(m), new_size)
self.assertEqual(m[:start_size], data)
self.assertEqual(m[start_size:], b'\0' * (new_size - start_size))
m = mmap.mmap(-1, start_size)
m[:] = data
m.resize(new_size)
self.assertEqual(len(m), new_size)
self.assertEqual(m[:start_size], data[:start_size])
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
def test_resize_up_private_anonymous_mapping(self):
start_size = PAGESIZE
new_size = 2 * start_size
data = random.randbytes(start_size)
with mmap.mmap(-1, start_size, flags=mmap.MAP_PRIVATE) as m:
m[:] = data
try:
m.resize(new_size)
except SystemError:
pass
else:
self.assertEqual(len(m), new_size)
self.assertEqual(m[:start_size], data)
self.assertEqual(m[start_size:], b'\0' * (new_size - start_size))
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_resize_down_anonymous_mapping(self):
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_resize_down_when_mapped_to_pagefile(self):
"""If the mmap is backed by the pagefile ensure a resize down up can happen
and that a truncated form of the original data is still in place
"""
start_size = 2 * PAGESIZE
start_size = PAGESIZE
new_size = start_size // 2
data = random.randbytes(start_size)
data = bytes(random.getrandbits(8) for _ in range(start_size))
with mmap.mmap(-1, start_size) as m:
m[:] = data
try:
m.resize(new_size)
except SystemError:
pass
else:
self.assertEqual(len(m), new_size)
self.assertEqual(m[:], data[:new_size])
if sys.platform.startswith(('linux', 'android')):
# Can't expand to its original size.
with self.assertRaises(ValueError):
m.resize(start_size)
m = mmap.mmap(-1, start_size)
m[:] = data
m.resize(new_size)
self.assertEqual(len(m), new_size)
self.assertEqual(m[:new_size], data[:new_size])
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_resize_fails_if_mapping_held_elsewhere(self):
"""If more than one mapping is held against a named file on Windows, neither
@@ -1023,7 +867,6 @@ class MmapTests(unittest.TestCase):
finally:
f.close()
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_resize_succeeds_with_error_for_second_named_mapping(self):
"""If a more than one mapping exists of the same name, none of them can
@@ -1045,168 +888,6 @@ class MmapTests(unittest.TestCase):
self.assertEqual(m1[:data_length], data)
self.assertEqual(m2[:data_length], data)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mmap_closed_by_int_scenarios(self):
"""
gh-103987: Test that mmap objects raise ValueError
for closed mmap files
"""
class MmapClosedByIntContext:
def __init__(self, access) -> None:
self.access = access
def __enter__(self):
self.f = open(TESTFN, "w+b")
self.f.write(random.randbytes(100))
self.f.flush()
m = mmap.mmap(self.f.fileno(), 100, access=self.access)
class X:
def __index__(self):
m.close()
return 10
return (m, X)
def __exit__(self, exc_type, exc_value, traceback):
self.f.close()
read_access_modes = [
mmap.ACCESS_READ,
mmap.ACCESS_WRITE,
mmap.ACCESS_COPY,
mmap.ACCESS_DEFAULT,
]
write_access_modes = [
mmap.ACCESS_WRITE,
mmap.ACCESS_COPY,
mmap.ACCESS_DEFAULT,
]
for access in read_access_modes:
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m[X()]
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m[X() : 20]
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m[X() : 20 : 2]
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m[20 : X() : -2]
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m.read(X())
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m.find(b"1", 1, X())
for access in write_access_modes:
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m[X() : 20] = b"1" * 10
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m[X() : 20 : 2] = b"1" * 5
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m[20 : X() : -2] = b"1" * 5
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m.move(1, 2, X())
with MmapClosedByIntContext(access) as (m, X):
with self.assertRaisesRegex(ValueError, "mmap closed or invalid"):
m.write_byte(X())
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
@unittest.skipUnless(hasattr(mmap.mmap, '_protect'), 'test needs debug build')
def test_access_violations(self):
from test.support.os_helper import TESTFN
code = textwrap.dedent("""
import faulthandler
import mmap
import os
import sys
from contextlib import suppress
# Prevent logging access violations to stderr.
faulthandler.disable()
PAGESIZE = mmap.PAGESIZE
PAGE_NOACCESS = 0x01
with open(sys.argv[1], 'bw+') as f:
f.write(b'A'* PAGESIZE)
f.flush()
m = mmap.mmap(f.fileno(), PAGESIZE)
m._protect(PAGE_NOACCESS, 0, PAGESIZE)
with suppress(OSError):
m.read(PAGESIZE)
assert False, 'mmap.read() did not raise'
with suppress(OSError):
m.read_byte()
assert False, 'mmap.read_byte() did not raise'
with suppress(OSError):
m.readline()
assert False, 'mmap.readline() did not raise'
with suppress(OSError):
m.write(b'A'* PAGESIZE)
assert False, 'mmap.write() did not raise'
with suppress(OSError):
m.write_byte(0)
assert False, 'mmap.write_byte() did not raise'
with suppress(OSError):
m[0] # test mmap_subscript
assert False, 'mmap.__getitem__() did not raise'
with suppress(OSError):
m[0:10] # test mmap_subscript
assert False, 'mmap.__getitem__() did not raise'
with suppress(OSError):
m[0:10:2] # test mmap_subscript
assert False, 'mmap.__getitem__() did not raise'
with suppress(OSError):
m[0] = 1
assert False, 'mmap.__setitem__() did not raise'
with suppress(OSError):
m[0:10] = b'A'* 10
assert False, 'mmap.__setitem__() did not raise'
with suppress(OSError):
m[0:10:2] = b'A'* 5
assert False, 'mmap.__setitem__() did not raise'
with suppress(OSError):
m.move(0, 10, 1)
assert False, 'mmap.move() did not raise'
with suppress(OSError):
list(m) # test mmap_item
assert False, 'mmap.__getitem__() did not raise'
with suppress(OSError):
m.find(b'A')
assert False, 'mmap.find() did not raise'
with suppress(OSError):
m.rfind(b'A')
assert False, 'mmap.rfind() did not raise'
""")
rt, stdout, stderr = assert_python_ok("-c", code, TESTFN)
self.assertEqual(stdout.strip(), b'')
self.assertEqual(stderr.strip(), b'')
class LargeMmapTests(unittest.TestCase):
def setUp(self):
@@ -1216,7 +897,7 @@ class LargeMmapTests(unittest.TestCase):
unlink(TESTFN)
def _make_test_file(self, num_zeroes, tail):
if sys.platform[:3] == 'win' or is_apple:
if sys.platform[:3] == 'win' or sys.platform == 'darwin':
requires('largefile',
'test requires %s bytes and a long time to run' % str(0x180000000))
f = open(TESTFN, 'w+b')

View File

@@ -3,5 +3,6 @@ from test._test_multiprocessing import install_tests_in_module_dict
install_tests_in_module_dict(globals(), 'fork', only_type="processes")
if __name__ == '__main__':
unittest.main()

View File

@@ -1934,11 +1934,6 @@ class _PosixSpawnMixin:
)
support.wait_process(pid, exitcode=0)
def test_setpgroup_allow_none(self):
path, args = self.NOOP_PROGRAM[0], self.NOOP_PROGRAM
pid = self.spawn_func(path, args, os.environ, setpgroup=None)
support.wait_process(pid, exitcode=0)
def test_setpgroup_wrong_type(self):
with self.assertRaises(TypeError):
self.spawn_func(sys.executable,
@@ -2039,21 +2034,6 @@ class _PosixSpawnMixin:
[sys.executable, "-c", "pass"],
os.environ, setsigdef=[signal.NSIG, signal.NSIG+1])
def test_scheduler_allow_none(self):
path, args = self.NOOP_PROGRAM[0], self.NOOP_PROGRAM
pid = self.spawn_func(path, args, os.environ, scheduler=None)
support.wait_process(pid, exitcode=0)
@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
@support.subTests("scheduler", [object(), 1, [1, 2]])
def test_scheduler_wrong_type(self, scheduler):
path, args = self.NOOP_PROGRAM[0], self.NOOP_PROGRAM
with self.assertRaisesRegex(
TypeError,
"scheduler must be a tuple or None",
):
self.spawn_func(path, args, os.environ, scheduler=scheduler)
@unittest.expectedFailureIf(sys.platform in ("darwin", "linux"), "TODO: RUSTPYTHON; NotImplementedError: scheduler parameter is not yet implemented")
@requires_sched
@unittest.skipIf(sys.platform.startswith(('freebsd', 'netbsd')),

View File

@@ -1,7 +1,5 @@
"""Unit tests for zero-argument super() & related machinery."""
import copy
import pickle
import textwrap
import threading
import unittest
@@ -9,6 +7,9 @@ from unittest.mock import patch
from test.support import import_helper, threading_helper
ADAPTIVE_WARMUP_DELAY = 2
class A:
def f(self):
return 'A'
@@ -90,7 +91,8 @@ class TestSuper(unittest.TestCase):
self.assertEqual(E().f(), 'AE')
@unittest.expectedFailure # TODO: RUSTPYTHON
# TODO: RUSTPYTHON; SyntaxError: name '__class__' is assigned to before global declaration
'''
def test_various___class___pathologies(self):
# See issue #12370
class X(A):
@@ -112,7 +114,7 @@ class TestSuper(unittest.TestCase):
__class__""", globals(), {})
self.assertIs(type(e.exception), NameError) # Not UnboundLocalError
class X:
# global __class__ # TODO: RUSTPYTHON; SyntaxError: name '__class__' is assigned to before global declaration
global __class__
__class__ = 42
def f():
__class__
@@ -120,11 +122,12 @@ class TestSuper(unittest.TestCase):
del globals()["__class__"]
self.assertNotIn("__class__", X.__dict__)
class X:
# nonlocal __class__ # TODO: RUSTPYTHON; SyntaxError: name '__class__' is assigned to before nonlocal declaration
nonlocal __class__
__class__ = 42
def f():
__class__
self.assertEqual(__class__, 42)
'''
def test___class___instancemethod(self):
# See issue #14857
@@ -188,7 +191,7 @@ class TestSuper(unittest.TestCase):
B = type("B", (), test_namespace)
self.assertIs(B.f(), B)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test___class___mro(self):
# See issue #23722
test_class = None
@@ -446,7 +449,7 @@ class TestSuper(unittest.TestCase):
self.assertEqual(C().method(), super)
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: type 'super' is not an acceptable base type
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: type 'super' is not an acceptable base type
def test_super_subclass___class__(self):
class mysuper(super):
pass
@@ -466,8 +469,7 @@ class TestSuper(unittest.TestCase):
super(MyType, type(mytype)).__setattr__(mytype, "bar", 1)
self.assertEqual(mytype.bar, 1)
_testinternalcapi = import_helper.import_module("_testinternalcapi")
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
for _ in range(ADAPTIVE_WARMUP_DELAY):
test("foo1")
def test_reassigned_new(self):
@@ -486,8 +488,7 @@ class TestSuper(unittest.TestCase):
def __new__(cls):
return super().__new__(cls)
_testinternalcapi = import_helper.import_module("_testinternalcapi")
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
for _ in range(ADAPTIVE_WARMUP_DELAY):
C()
def test_mixed_staticmethod_hierarchy(self):
@@ -507,8 +508,7 @@ class TestSuper(unittest.TestCase):
def some(cls):
return super().some(cls)
_testinternalcapi = import_helper.import_module("_testinternalcapi")
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
for _ in range(ADAPTIVE_WARMUP_DELAY):
C.some(C)
@threading_helper.requires_working_threading()
@@ -544,74 +544,6 @@ class TestSuper(unittest.TestCase):
for thread in threads:
thread.join()
def test_special_methods(self):
for e in E(), E:
s = super(C, e)
self.assertEqual(s.__reduce__, e.__reduce__)
self.assertEqual(s.__reduce_ex__, e.__reduce_ex__)
self.assertEqual(s.__getstate__, e.__getstate__)
self.assertNotHasAttr(s, '__getnewargs__')
self.assertNotHasAttr(s, '__getnewargs_ex__')
self.assertNotHasAttr(s, '__setstate__')
self.assertNotHasAttr(s, '__copy__')
self.assertNotHasAttr(s, '__deepcopy__')
def test_pickling(self):
e = E()
e.x = 1
s = super(C, e)
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
u = pickle.loads(pickle.dumps(s, proto))
self.assertEqual(u.f(), s.f())
self.assertIs(type(u), type(s))
self.assertIs(type(u.__self__), E)
self.assertEqual(u.__self__.x, 1)
self.assertIs(u.__thisclass__, C)
self.assertIs(u.__self_class__, E)
s = super(C, E)
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
u = pickle.loads(pickle.dumps(s, proto))
self.assertEqual(u.cm(), s.cm())
self.assertEqual(u.f, s.f)
self.assertIs(type(u), type(s))
self.assertIs(u.__self__, E)
self.assertIs(u.__thisclass__, C)
self.assertIs(u.__self_class__, E)
def test_shallow_copying(self):
s = super(C, E())
self.assertIs(copy.copy(s), s)
s = super(C, E)
self.assertIs(copy.copy(s), s)
def test_deep_copying(self):
e = E()
e.x = [1]
s = super(C, e)
u = copy.deepcopy(s)
self.assertEqual(u.f(), s.f())
self.assertIs(type(u), type(s))
self.assertIsNot(u, s)
self.assertIs(type(u.__self__), E)
self.assertIsNot(u.__self__, e)
self.assertIsNot(u.__self__.x, e.x)
self.assertEqual(u.__self__.x, [1])
self.assertIs(u.__thisclass__, C)
self.assertIs(u.__self_class__, E)
s = super(C, E)
u = copy.deepcopy(s)
self.assertEqual(u.cm(), s.cm())
self.assertEqual(u.f, s.f)
self.assertIsNot(u, s)
self.assertIs(type(u), type(s))
self.assertIs(u.__self__, E)
self.assertIs(u.__thisclass__, C)
self.assertIs(u.__self_class__, E)
if __name__ == "__main__":
unittest.main()

84
Lib/test/test_uuid.py vendored
View File

@@ -1176,47 +1176,6 @@ class CommandLineTestCases:
self.assertEqual(cm.exception.code, 2)
self.assertIn("error: Incorrect number of arguments", mock_err.getvalue())
@mock.patch.object(sys, "argv",
["", "-u", "uuid3", "-n", "@dns", "-N", "python.org"])
def test_cli_uuid3_outputted_with_valid_namespace_and_name(self):
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
self.uuid.main()
output = stdout.getvalue().strip()
uuid_output = self.uuid.UUID(output)
# Output should be in the form of uuid3
self.assertEqual(output, str(uuid_output))
self.assertEqual(uuid_output.version, 3)
@mock.patch.object(sys, "argv",
["", "-u", "uuid3", "-n",
"0d6a16cc-34a7-47d8-b660-214d0ae184d2",
"-N", "some.user"])
def test_cli_uuid3_outputted_with_custom_namespace_and_name(self):
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
self.uuid.main()
output = stdout.getvalue().strip()
uuid_output = self.uuid.UUID(output)
# Output should be in the form of uuid3
self.assertEqual(output, str(uuid_output))
self.assertEqual(uuid_output.version, 3)
@mock.patch.object(sys, "argv",
["", "-u", "uuid3", "-n", "any UUID", "-N", "python.org"])
@mock.patch('sys.stderr', new_callable=io.StringIO)
def test_cli_uuid3_with_invalid_namespace(self, mock_err):
with self.assertRaises(SystemExit) as cm:
self.uuid.main()
# Check that exception code is the same as argparse.ArgumentParser.error
self.assertEqual(cm.exception.code, 2)
self.assertIn("error: badly formed hexadecimal UUID string",
mock_err.getvalue())
@mock.patch.object(sys, "argv", [""])
def test_cli_uuid4_outputted_with_no_args(self):
stdout = io.StringIO()
@@ -1244,9 +1203,23 @@ class CommandLineTestCases:
uuid_output = self.uuid.UUID(o)
self.assertEqual(uuid_output.version, 4)
@mock.patch.object(sys, "argv",
["", "-u", "uuid3", "-n", "@dns", "-N", "python.org"])
def test_cli_uuid3_ouputted_with_valid_namespace_and_name(self):
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
self.uuid.main()
output = stdout.getvalue().strip()
uuid_output = self.uuid.UUID(output)
# Output should be in the form of uuid5
self.assertEqual(output, str(uuid_output))
self.assertEqual(uuid_output.version, 3)
@mock.patch.object(sys, "argv",
["", "-u", "uuid5", "-n", "@dns", "-N", "python.org"])
def test_cli_uuid5_outputted_with_valid_namespace_and_name(self):
def test_cli_uuid5_ouputted_with_valid_namespace_and_name(self):
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
self.uuid.main()
@@ -1258,33 +1231,6 @@ class CommandLineTestCases:
self.assertEqual(output, str(uuid_output))
self.assertEqual(uuid_output.version, 5)
@mock.patch.object(sys, "argv",
["", "-u", "uuid5", "-n",
"0d6a16cc-34a7-47d8-b660-214d0ae184d2",
"-N", "some.user"])
def test_cli_uuid5_ouputted_with_custom_namespace_and_name(self):
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
self.uuid.main()
output = stdout.getvalue().strip()
uuid_output = self.uuid.UUID(output)
# Output should be in the form of uuid5
self.assertEqual(output, str(uuid_output))
self.assertEqual(uuid_output.version, 5)
@mock.patch.object(sys, "argv",
["", "-u", "uuid5", "-n", "any UUID", "-N", "python.org"])
@mock.patch('sys.stderr', new_callable=io.StringIO)
def test_cli_uuid5_with_invalid_namespace(self, mock_err):
with self.assertRaises(SystemExit) as cm:
self.uuid.main()
# Check that exception code is the same as argparse.ArgumentParser.error
self.assertEqual(cm.exception.code, 2)
self.assertIn("error: badly formed hexadecimal UUID string",
mock_err.getvalue())
@mock.patch.object(sys, "argv", ["", "-u", "uuid6"])
def test_cli_uuid6(self):
self.do_test_standalone_uuid(6)

13
Lib/test/test_venv.py vendored
View File

@@ -266,7 +266,6 @@ class BasicTest(BaseTest):
with patch('venv.subprocess.check_output', pip_cmd_checker):
builder.upgrade_dependencies(fake_context)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@requireVenvCreate
def test_prefixes(self):
"""
@@ -286,7 +285,6 @@ class BasicTest(BaseTest):
self.assertEqual(pathlib.Path(out.strip().decode()),
pathlib.Path(expected), prefix)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@requireVenvCreate
def test_sysconfig(self):
"""
@@ -320,7 +318,6 @@ class BasicTest(BaseTest):
out, err = check_output(cmd, encoding='utf-8')
self.assertEqual(out.strip(), expected, err)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@requireVenvCreate
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
def test_sysconfig_symlinks(self):
@@ -461,7 +458,6 @@ class BasicTest(BaseTest):
data = self.get_text_file_contents('pyvenv.cfg')
self.assertIn('include-system-site-packages = %s\n' % s, data)
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
def test_symlinking(self):
"""
@@ -486,7 +482,6 @@ class BasicTest(BaseTest):
# run the test, the pyvenv.cfg in the venv created in the test will
# point to the venv being used to run the test, and we lose the link
# to the source build - so Python can't initialise properly.
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@requireVenvCreate
def test_executable(self):
"""
@@ -499,7 +494,6 @@ class BasicTest(BaseTest):
'import sys; print(sys.executable)'])
self.assertEqual(out.strip(), envpy.encode())
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
def test_executable_symlinks(self):
"""
@@ -568,7 +562,6 @@ class BasicTest(BaseTest):
self.assertEndsWith(lines[1], env_name.encode())
# gh-124651: test quoted strings on Windows
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
def test_special_chars_windows(self):
"""
@@ -592,7 +585,6 @@ class BasicTest(BaseTest):
self.assertTrue(env_name.encode() in lines[0])
self.assertEndsWith(lines[1], env_name.encode())
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
def test_unicode_in_batch_file(self):
"""
@@ -624,7 +616,6 @@ class BasicTest(BaseTest):
filepath_regex = r"'[A-Z]:\\\\(?:[^\\\\]+\\\\)*[^\\\\]+'"
self.assertRegex(err, rf"Unable to symlink {filepath_regex} to {filepath_regex}")
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@requireVenvCreate
def test_multiprocessing(self):
"""
@@ -644,7 +635,6 @@ class BasicTest(BaseTest):
'pool.terminate()'])
self.assertEqual(out.strip(), "python".encode())
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@requireVenvCreate
def test_multiprocessing_recursion(self):
"""
@@ -902,7 +892,6 @@ class BasicTest(BaseTest):
self.assertFalse(same_path(path1, path2))
# gh-126084: venvwlauncher should run pythonw, not python
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
@requireVenvCreate
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
def test_venvwlauncher(self):
@@ -937,13 +926,11 @@ class EnsurePipTest(BaseTest):
self.assertEqual(out.strip(), "OK")
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_no_pip_by_default(self):
rmtree(self.env_dir)
self.run_with_capture(venv.create, self.env_dir)
self.assert_pip_not_installed()
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_explicit_no_pip(self):
rmtree(self.env_dir)
self.run_with_capture(venv.create, self.env_dir, with_pip=False)

10
Lib/uuid.py vendored
View File

@@ -961,7 +961,7 @@ def main():
default="uuid4",
help="function to generate the UUID")
parser.add_argument("-n", "--namespace",
metavar=f"{{any UUID,{','.join(namespaces)}}}",
choices=["any UUID", *namespaces.keys()],
help="uuid3/uuid5 only: "
"a UUID, or a well-known predefined UUID addressed "
"by namespace name")
@@ -983,13 +983,7 @@ def main():
f"{args.uuid} requires a namespace and a name. "
"Run 'python -m uuid -h' for more information."
)
if namespace in namespaces:
namespace = namespaces[namespace]
else:
try:
namespace = UUID(namespace)
except ValueError as exc:
parser.error(f"{exc}: {args.namespace!r}")
namespace = namespaces[namespace] if namespace in namespaces else UUID(namespace)
for _ in range(args.count):
print(uuid_func(namespace, name))
else:

View File

@@ -13,7 +13,6 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
bitflags = { workspace = true }
itertools = { workspace = true }
num-complex = { workspace = true }
rustpython-vm = { workspace = true, features = ["threading", "compiler"] }
rustpython-stdlib = {workspace = true, features = ["threading"] }

View File

@@ -1,14 +1,9 @@
use crate::{PyObject, pystate::with_vm};
use alloc::slice;
use core::ffi::c_int;
pub use mapping::*;
use rustpython_vm::builtins::{PyDict, PyStr, PyTuple};
use rustpython_vm::function::{FuncArgs, KwArgs, PosArgs};
use rustpython_vm::{AsObject, Py, PyObjectRef, PyResult, VirtualMachine};
pub use sequence::*;
mod mapping;
mod sequence;
const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1);

View File

@@ -1,66 +0,0 @@
use crate::{PyObject, pystate::with_vm};
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyMapping_Size(obj: *mut PyObject) -> isize {
with_vm(|vm| {
let obj = unsafe { &*obj };
obj.try_mapping(vm)?.length(vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyMapping_Keys(obj: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
let keys = obj.try_mapping(vm)?.keys(vm)?;
let iter = keys.get_iter(vm)?;
Ok(vm.ctx.new_list(iter.try_to_value(vm)?))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyMapping_Values(obj: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
let values = obj.try_mapping(vm)?.values(vm)?;
let iter = values.get_iter(vm)?;
Ok(vm.ctx.new_list(iter.try_to_value(vm)?))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyMapping_Items(obj: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
let items = obj.try_mapping(vm)?.items(vm)?;
let iter = items.get_iter(vm)?;
Ok(vm.ctx.new_list(iter.try_to_value(vm)?))
})
}
#[cfg(false)]
mod tests {
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyMapping, PyMappingMethods, PyTuple};
#[test]
fn size_keys_values_items() {
Python::attach(|py| {
let dict = PyDict::new(py);
dict.set_item("a", 1).unwrap();
dict.set_item("b", 2).unwrap();
let mapping = dict.cast_into::<PyMapping>().unwrap();
assert_eq!(mapping.len().unwrap(), 2);
let keys = mapping.keys().unwrap();
assert_eq!(keys.len(), 2);
let values = mapping.values().unwrap();
assert_eq!(values.len(), 2);
let items = mapping.items().unwrap();
assert_eq!(items.len(), 2);
assert!(items.iter().all(|item| item.cast_into::<PyTuple>().is_ok()));
})
}
}

View File

@@ -1,286 +0,0 @@
use crate::{PyObject, pystate::with_vm};
use core::ffi::c_int;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Check(obj: *mut PyObject) -> c_int {
with_vm(|_vm| {
let obj = unsafe { &*obj };
Ok(obj.sequence_unchecked().check())
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Concat(
obj1: *mut PyObject,
obj2: *mut PyObject,
) -> *mut PyObject {
with_vm(|vm| {
let obj1 = unsafe { &*obj1 };
let obj2 = unsafe { &*obj2 };
obj1.try_sequence(vm)?.concat(obj2, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Count(obj: *mut PyObject, value: *mut PyObject) -> isize {
with_vm(|vm| {
let obj = unsafe { &*obj };
let value = unsafe { &*value };
obj.try_sequence(vm)?.count(value, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_DelItem(obj: *mut PyObject, index: isize) -> c_int {
with_vm(|vm| {
let obj = unsafe { &*obj };
obj.try_sequence(vm)?.del_item(index, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_DelSlice(obj: *mut PyObject, low: isize, high: isize) -> c_int {
with_vm(|vm| {
let obj = unsafe { &*obj };
obj.try_sequence(vm)?.del_slice(low, high, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_GetItem(obj: *mut PyObject, index: isize) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
obj.try_sequence(vm)?.get_item(index, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_GetSlice(
obj: *mut PyObject,
low: isize,
high: isize,
) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
obj.try_sequence(vm)?.get_slice(low, high, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_InPlaceConcat(
obj1: *mut PyObject,
obj2: *mut PyObject,
) -> *mut PyObject {
with_vm(|vm| {
let obj1 = unsafe { &*obj1 };
let obj2 = unsafe { &*obj2 };
obj1.try_sequence(vm)?.inplace_concat(obj2, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_InPlaceRepeat(
obj: *mut PyObject,
count: isize,
) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
obj.try_sequence(vm)?.inplace_repeat(count, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Index(obj: *mut PyObject, value: *mut PyObject) -> isize {
with_vm(|vm| {
let obj = unsafe { &*obj };
let value = unsafe { &*value };
obj.try_sequence(vm)?.index(value, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_List(obj: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
Ok(obj.try_sequence(vm)?.list(vm))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Repeat(obj: *mut PyObject, count: isize) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
obj.try_sequence(vm)?.repeat(count, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_SetItem(
obj: *mut PyObject,
index: isize,
value: *mut PyObject,
) -> c_int {
with_vm(|vm| {
let obj = unsafe { &*obj };
let value = unsafe { &*value };
obj.try_sequence(vm)?.set_item(index, value.to_owned(), vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_SetSlice(
obj: *mut PyObject,
low: isize,
high: isize,
value: *mut PyObject,
) -> c_int {
with_vm(|vm| {
let obj = unsafe { &*obj };
let value = unsafe { &*value };
obj.try_sequence(vm)?
.set_slice(low, high, value.to_owned(), vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Size(obj: *mut PyObject) -> isize {
with_vm(|vm| {
let obj = unsafe { &*obj };
obj.try_sequence(vm)?.length(vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Length(obj: *mut PyObject) -> isize {
unsafe { PySequence_Size(obj) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Tuple(obj: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let obj = unsafe { &*obj };
Ok(obj.try_sequence(vm)?.tuple(vm))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_Contains(obj: *mut PyObject, value: *mut PyObject) -> c_int {
with_vm(|vm| {
let obj = unsafe { &*obj };
let value = unsafe { &*value };
obj.sequence_unchecked().contains(value, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySequence_In(obj: *mut PyObject, value: *mut PyObject) -> c_int {
unsafe { PySequence_Contains(obj, value) }
}
#[cfg(false)]
mod tests {
use pyo3::prelude::*;
use pyo3::types::{PyAnyMethods, PyDict, PyList, PySequence, PySequenceMethods, PyTuple};
#[test]
fn item_and_size_ops() {
Python::attach(|py| {
let list = PyList::new(py, [1, 2, 3]).unwrap();
let seq = list.cast_into::<PySequence>().unwrap();
assert_eq!(seq.len().unwrap(), 3);
assert_eq!(seq.get_item(1).unwrap().extract::<i32>().unwrap(), 2);
seq.set_item(1, 4).unwrap();
assert_eq!(seq.get_item(1).unwrap().extract::<i32>().unwrap(), 4);
seq.del_item(1).unwrap();
assert_eq!(seq.get_item(1).unwrap().extract::<i32>().unwrap(), 3);
});
}
#[test]
fn slice_ops() {
Python::attach(|py| {
let list = PyList::new(py, [1, 2, 3, 4]).unwrap();
let seq = list.cast_into::<PySequence>().unwrap();
let sub = seq.get_slice(1, 3).unwrap();
assert_eq!(sub.get_item(0).unwrap().extract::<i32>().unwrap(), 2);
assert_eq!(sub.get_item(1).unwrap().extract::<i32>().unwrap(), 3);
let repl = PyList::new(py, [8, 9]).unwrap();
seq.set_slice(1, 3, &repl).unwrap();
assert_eq!(seq.get_item(1).unwrap().extract::<i32>().unwrap(), 8);
assert_eq!(seq.get_item(2).unwrap().extract::<i32>().unwrap(), 9);
seq.del_slice(1, 3).unwrap();
assert_eq!(seq.get_item(0).unwrap().extract::<i32>().unwrap(), 1);
assert_eq!(seq.get_item(1).unwrap().extract::<i32>().unwrap(), 4);
});
}
#[test]
fn concat_repeat_and_inplace_ops() {
Python::attach(|py| {
let list = PyList::new(py, [1, 2]).unwrap();
let seq = list.cast_into::<PySequence>().unwrap();
let rhs = PyList::new(py, [3])
.unwrap()
.cast_into::<PySequence>()
.unwrap();
let concat = seq.concat(&rhs).unwrap();
assert_eq!(concat.get_item(2).unwrap().extract::<i32>().unwrap(), 3);
let repeat = seq.repeat(2).unwrap();
assert_eq!(repeat.get_item(2).unwrap().extract::<i32>().unwrap(), 1);
let iadd_rhs = PyList::new(py, [4])
.unwrap()
.cast_into::<PySequence>()
.unwrap();
seq.in_place_concat(&iadd_rhs).unwrap();
assert_eq!(seq.get_item(2).unwrap().extract::<i32>().unwrap(), 4);
seq.in_place_repeat(2).unwrap();
assert_eq!(seq.get_item(5).unwrap().extract::<i32>().unwrap(), 4);
});
}
#[test]
fn count_index_contains_and_convert_ops() {
Python::attach(|py| {
let tuple = PyTuple::new(py, [1, 2, 1])
.unwrap()
.cast_into::<PySequence>()
.unwrap();
assert_eq!(tuple.count(1).unwrap(), 2);
assert_eq!(tuple.index(2).unwrap(), 1);
assert!(tuple.contains(1).unwrap());
let as_list = tuple.to_list().unwrap().cast_into::<PySequence>().unwrap();
assert_eq!(as_list.get_item(0).unwrap().extract::<i32>().unwrap(), 1);
let as_tuple = as_list
.to_tuple()
.unwrap()
.cast_into::<PySequence>()
.unwrap();
assert_eq!(as_tuple.get_item(2).unwrap().extract::<i32>().unwrap(), 1);
});
}
#[test]
fn contains_works_for_dict() {
Python::attach(|py| {
let dict = PyDict::new(py);
dict.set_item("k", 1).unwrap();
let any = dict.into_any();
assert!(any.contains("k").unwrap());
assert!(!any.contains("missing").unwrap());
});
}
}

View File

@@ -25,12 +25,10 @@ pub mod pyerrors;
pub mod pylifecycle;
pub mod pystate;
pub mod refcount;
pub mod setobject;
pub mod traceback;
pub mod tupleobject;
pub mod unicodeobject;
mod util;
pub mod weakrefobject;
/// Get main interpreter of this process. Will be None if it has not been initialized yet.
pub fn get_main_interpreter() -> MutexGuard<'static, Option<Interpreter>> {

View File

@@ -1,162 +0,0 @@
use crate::PyObject;
use crate::object::define_py_check;
use crate::pystate::with_vm;
use core::ffi::c_int;
use itertools::process_results;
use rustpython_vm::AsObject;
use rustpython_vm::PyPayload;
use rustpython_vm::TryFromObject;
use rustpython_vm::builtins::{PyFrozenSet, PySet};
use rustpython_vm::function::ArgIterable;
define_py_check!(fn PySet_Check, types.set_type);
define_py_check!(fn PyFrozenSet_Check, types.frozenset_type);
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySet_New(iterable: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
if iterable.is_null() {
return Ok(PySet::default().into_ref(&vm.ctx));
}
let iterable = ArgIterable::try_from_object(vm, unsafe { &*iterable }.to_owned())?;
let set = PySet::default().into_ref(&vm.ctx);
for item in iterable.iter(vm)? {
set.add(item?, vm)?;
}
Ok(set)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyFrozenSet_New(iterable: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
if iterable.is_null() {
return Ok(vm.ctx.empty_frozenset.to_owned());
}
let iterable = ArgIterable::try_from_object(vm, unsafe { &*iterable }.to_owned())?;
let set = process_results(iterable.iter(vm)?, |it| PyFrozenSet::from_iter(vm, it))??;
Ok(set.into_ref(&vm.ctx))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySet_Add(set: *mut PyObject, key: *mut PyObject) -> c_int {
with_vm(|vm| {
let set = unsafe { &*set }.try_downcast_ref::<PySet>(vm)?;
let key = unsafe { &*key }.to_owned();
set.add(key, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySet_Clear(set: *mut PyObject) -> c_int {
with_vm(|vm| {
let set = unsafe { &*set }.try_downcast_ref::<PySet>(vm)?;
set.clear();
Ok(())
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySet_Contains(anyset: *mut PyObject, key: *mut PyObject) -> c_int {
with_vm(|vm| {
let anyset = unsafe { &*anyset };
let key = unsafe { &*key };
if let Some(set) = anyset.downcast_ref::<PySet>() {
set.__contains__(key, vm)
} else if let Some(frozenset) = anyset.downcast_ref::<PyFrozenSet>() {
frozenset.__contains__(key, vm)
} else {
Err(vm.new_type_error(format!(
"expected set or frozenset, got '{}'",
anyset.class().name()
)))
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySet_Discard(set: *mut PyObject, key: *mut PyObject) -> c_int {
with_vm(|vm| {
let set = unsafe { &*set }.try_downcast_ref::<PySet>(vm)?;
let key = unsafe { &*key };
let had_item = set.__contains__(key, vm)?;
if had_item {
set.discard(key.to_owned(), vm)?;
}
Ok(had_item)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySet_Pop(set: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let set = unsafe { &*set }.try_downcast_ref::<PySet>(vm)?;
set.pop(vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PySet_Size(anyset: *mut PyObject) -> isize {
with_vm(|vm| {
let anyset = unsafe { &*anyset };
if let Some(set) = anyset.downcast_ref::<PySet>() {
set.as_object().length(vm)
} else if let Some(frozenset) = anyset.downcast_ref::<PyFrozenSet>() {
frozenset.as_object().length(vm)
} else {
Err(vm.new_type_error(format!(
"expected set or frozenset, got '{}'",
anyset.class().name()
)))
}
})
}
#[cfg(false)]
mod tests {
use pyo3::prelude::*;
use pyo3::types::{PyFrozenSet, PyInt, PySet};
#[test]
fn new_and_size() {
Python::attach(|py| {
let set = PySet::empty(py).unwrap();
assert!(set.is_instance_of::<PySet>());
assert_eq!(set.len(), 0);
let frozen = PyFrozenSet::empty(py).unwrap();
assert!(frozen.is_instance_of::<PyFrozenSet>());
assert_eq!(frozen.len(), 0);
})
}
#[test]
fn add_contains_discard() {
Python::attach(|py| {
let set = PySet::empty(py).unwrap();
let item = PyInt::new(py, 42);
set.add(&item).unwrap();
assert!(set.contains(&item).unwrap());
set.discard(&item).unwrap();
assert!(!set.contains(&item).unwrap());
})
}
#[test]
fn pop_reduces_size() {
Python::attach(|py| {
let set = PySet::empty(py).unwrap();
set.add(7).unwrap();
assert_eq!(set.len(), 1);
let popped = set.pop().unwrap();
assert_eq!(popped.extract::<i32>().unwrap(), 7);
assert_eq!(set.len(), 0);
})
}
}

View File

@@ -1,106 +0,0 @@
use crate::PyObject;
use crate::object::define_py_check;
use crate::pystate::with_vm;
use core::ffi::c_int;
use rustpython_vm::builtins::{PyWeak, PyWeakProxy};
define_py_check!(fn PyWeakref_CheckProxy, types.weakproxy_type);
define_py_check!(fn PyWeakref_CheckRef, types.weakref_type);
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyWeakref_GetRef(
reference: *mut PyObject,
result: *mut *mut PyObject,
) -> c_int {
with_vm(|vm| {
unsafe {
*result = core::ptr::null_mut();
}
let reference = unsafe { &*reference };
let upgraded = if let Some(weak) = reference.downcast_ref::<PyWeak>() {
weak.upgrade()
} else if let Some(proxy) = reference.downcast_ref::<PyWeakProxy>() {
proxy.get_weak().upgrade()
} else {
return Err(vm.new_type_error("expected a weakref"));
};
if let Some(obj) = upgraded {
unsafe {
*result = obj.into_raw().as_ptr();
}
Ok(true)
} else {
Ok(false)
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyWeakref_NewProxy(
ob: *mut PyObject,
callback: *mut PyObject,
) -> *mut PyObject {
with_vm(|vm| {
let ob = unsafe { &*ob };
let callback = unsafe { callback.as_ref() }
.filter(|callback| !vm.is_none(callback))
.map(ToOwned::to_owned);
PyWeakProxy::new_weakproxy(ob, callback, vm)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyWeakref_NewRef(
ob: *mut PyObject,
callback: *mut PyObject,
) -> *mut PyObject {
with_vm(|vm| {
let ob = unsafe { &*ob };
let callback = unsafe { callback.as_ref() }
.filter(|callback| !vm.is_none(callback))
.map(ToOwned::to_owned);
ob.downgrade(callback, vm)
})
}
#[cfg(false)]
mod tests {
use pyo3::prelude::*;
use pyo3::types::PyAnyMethods;
use pyo3::types::{PyInt, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefReference};
#[test]
fn check_ref_and_proxy() {
Python::attach(|py| {
let object_ty = py.get_type::<PyInt>();
let weak_ref = PyWeakrefReference::new(&object_ty).unwrap();
let weak_proxy = PyWeakrefProxy::new(&object_ty).unwrap();
assert!(weak_ref.is_instance_of::<PyWeakrefReference>());
assert!(weak_proxy.is_instance_of::<PyWeakrefProxy>());
});
}
#[test]
fn new_ref_and_get_ref() {
Python::attach(|py| {
let object_ty = py.get_type::<PyInt>();
let weak_ref = PyWeakrefReference::new(&object_ty).unwrap();
assert!(weak_ref.upgrade().is_some_and(|obj| obj.is(&object_ty)));
});
}
#[test]
fn new_proxy() {
Python::attach(|py| {
let object_ty = py.get_type::<PyInt>();
let weak_proxy = PyWeakrefProxy::new(&object_ty).unwrap();
assert!(weak_proxy.upgrade().is_some_and(|obj| obj.is(&object_ty)));
});
}
}

View File

@@ -428,7 +428,6 @@ pub(crate) const fn is_uni_linebreak(ch: u32) -> bool {
}
#[inline]
pub(crate) fn is_uni_alnum(ch: u32) -> bool {
// TODO: check with cpython
char::try_from(ch).is_ok_and(|c| {
GeneralCategoryGroup::Letter
.union(GeneralCategoryGroup::Number)

View File

@@ -1,309 +0,0 @@
pub(crate) use _queue::module_def;
#[pymodule]
mod _queue {
use alloc::collections::VecDeque;
use core::time::Duration;
use std::time::Instant;
use crate::vm::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyBaseExceptionRef, PyException, PyGenericAlias, PyStr, PyType, PyTypeRef},
function::{PyComparisonValue, TimeoutSeconds},
protocol::PyNumberMethods,
types::{AsNumber, Comparable, Constructor, PyComparisonOp, Representable},
};
type BufInner = VecDeque<PyObjectRef>;
cfg_select! {
feature = "threading" => {
use parking_lot::{Condvar, Mutex, MutexGuard};
type Buf = Mutex<BufInner>;
},
_ => {
use crate::common::lock::PyMutex;
type Buf = PyMutex<BufInner>;
}
}
const INITIAL_RING_BUF_CAPACITY: usize = 8;
#[pyattr]
#[pyclass(module = "_queue", name = "Empty", base = PyException)]
#[repr(transparent)]
pub(crate) struct PyEmptyError(PyException);
#[pyclass(flags(HAS_WEAKREF))]
impl PyEmptyError {}
/// ## See Also
///
/// [`empty_error`](https://github.com/python/cpython/blob/v3.14.5/Modules/_queuemodule.c#L347-L355).
fn empty_error(vm: &VirtualMachine) -> PyBaseExceptionRef {
vm.new_exception_empty(PyEmptyError::class(&vm.ctx).to_owned())
}
#[cfg(feature = "threading")]
#[derive(Debug)]
struct Semaphore {
mutex: Mutex<usize>,
cond: Condvar,
}
#[cfg(feature = "threading")]
impl Semaphore {
#[must_use]
fn new() -> Self {
Self {
mutex: Mutex::new(0),
cond: Condvar::new(),
}
}
fn release(&self) {
{
let mut count = self.mutex.lock();
*count += 1;
} // lock dropped. now we can notify a waiting thread
self.cond.notify_one();
}
/// Returns `true` if the semaphore was acquired, `false` on timeout.
#[must_use]
fn acquire(&self, block: bool, deadline: Option<Instant>, vm: &VirtualMachine) -> bool {
let mut count = self.mutex.lock();
loop {
if *count > 0 {
*count -= 1;
return true;
}
if !block {
return false;
}
match deadline {
Some(dl) => {
let result = vm.allow_threads(|| self.cond.wait_until(&mut count, dl));
if result.timed_out() && *count == 0 {
return false;
}
}
None => {
vm.allow_threads(|| self.cond.wait(&mut count));
}
}
}
}
}
#[pyattr]
#[pyclass(module = "_queue", name = "SimpleQueue", unhashable = true)]
#[derive(Debug, PyPayload)]
struct PySimpleQueue {
buf: Buf,
#[cfg(feature = "threading")]
sem: Semaphore,
}
impl Default for PySimpleQueue {
fn default() -> Self {
Self {
buf: Buf::new(VecDeque::with_capacity(INITIAL_RING_BUF_CAPACITY)),
#[cfg(feature = "threading")]
sem: Semaphore::new(),
}
}
}
impl PySimpleQueue {
fn push(&self, item: PyObjectRef) {
self.buf.lock().push_back(item);
#[cfg(feature = "threading")]
self.sem.release();
}
/// Returns a strong reference from the head of the buffer.
///
/// ## See Also
///
/// [`RingBuf_Get`](https://github.com/python/cpython/blob/v3.14.5/Modules/_queuemodule.c#L133-L154).
fn get_inner(
#[cfg(feature = "threading")] buf: &mut MutexGuard<'_, BufInner>,
#[cfg(not(feature = "threading"))] buf: &mut BufInner,
) -> Option<PyObjectRef> {
let cap = buf.capacity();
if buf.len() < (cap / 4) {
// Items is less than 25% occupied, shrink it by 50%. This allows for
// growth without immediately needing to resize the underlying items array
buf.shrink_to(cap / 2)
}
buf.pop_front()
}
}
#[derive(FromArgs)]
struct PutArgs {
#[pyarg(positional)]
item: PyObjectRef,
#[expect(
dead_code,
reason = "Intentional. Provide compatibility with the Queue class"
)]
#[pyarg(any, optional, default = true)]
block: bool,
#[expect(
dead_code,
reason = "Intentional. Provide compatibility with the Queue class"
)]
#[pyarg(any, optional)]
timeout: Option<PyObjectRef>,
}
#[derive(FromArgs)]
struct GetArgs {
#[pyarg(any, optional, default = true)]
block: bool,
#[pyarg(any, optional)]
timeout: Option<TimeoutSeconds>,
}
#[pyclass(
with(Constructor, Comparable, Representable),
flags(BASETYPE, HAS_WEAKREF, IMMUTABLETYPE)
)]
impl PySimpleQueue {
#[pymethod]
fn empty(&self) -> bool {
self.buf.lock().is_empty()
}
#[pymethod]
fn qsize(&self) -> usize {
self.buf.lock().len()
}
#[pymethod]
fn put(&self, args: PutArgs) {
let PutArgs { item, .. } = args;
self.push(item);
}
#[pymethod]
fn put_nowait(&self, item: PyObjectRef) {
self.push(item);
}
#[pymethod]
fn get(&self, args: GetArgs, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
let GetArgs { block, timeout } = args;
// Non-blocking: just try once
if !block {
return Self::get_inner(&mut self.buf.lock()).ok_or_else(|| empty_error(vm));
}
#[cfg_attr(
not(feature = "threading"),
expect(
unused_variables,
reason = "We are still validating the 'timeout' arg even if we don't have threading"
)
)]
let deadline = match timeout.map(|v| v.to_secs_f64()) {
Some(v) if v < 0.0 => {
return Err(vm.new_value_error("'timeout' must be a non-negative number"));
}
Some(v) => Some(Instant::now() + Duration::from_secs_f64(v)),
None => None,
};
#[cfg(feature = "threading")]
{
if !self.sem.acquire(block, deadline, vm) {
return Err(empty_error(vm));
}
}
Self::get_inner(&mut self.buf.lock()).ok_or_else(|| empty_error(vm))
}
#[pymethod]
fn get_nowait(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
#[cfg(feature = "threading")]
{
if !self.sem.acquire(false, None, vm) {
return Err(empty_error(vm));
}
}
Self::get_inner(&mut self.buf.lock()).ok_or_else(|| empty_error(vm))
}
#[pyclassmethod]
fn __class_getitem__(
cls: PyTypeRef,
args: PyObjectRef,
vm: &VirtualMachine,
) -> PyGenericAlias {
PyGenericAlias::from_args(cls, args, vm)
}
}
impl Constructor for PySimpleQueue {
type Args = ();
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
Ok(Self::default())
}
}
impl AsNumber for PySimpleQueue {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
boolean: Some(|number, _vm| {
let zelf = number.obj.downcast_ref::<PySimpleQueue>().unwrap();
Ok(!zelf.buf.lock().is_empty())
}),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER
}
}
impl Comparable for PySimpleQueue {
fn cmp(
zelf: &Py<Self>,
other: &PyObject,
op: PyComparisonOp,
_vm: &VirtualMachine,
) -> PyResult<PyComparisonValue> {
Ok(if let Some(res) = op.identical_optimization(zelf, other) {
res.into()
} else {
PyComparisonValue::NotImplemented
})
}
}
impl Representable for PySimpleQueue {
fn repr(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
Ok(vm.ctx.new_str(format!(
"<{} at {:#x}>",
Self::class(&vm.ctx).slot_name(),
zelf.get_id()
)))
}
fn repr_str(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
unreachable!("repr() is overridden directly")
}
}
}

View File

@@ -53,7 +53,6 @@ mod math;
#[cfg(all(feature = "host_env", any(unix, windows)))]
mod mmap;
mod _queue;
mod pyexpat;
mod pystruct;
mod random;
@@ -226,7 +225,6 @@ pub fn stdlib_module_defs(ctx: &Context) -> Vec<&'static builtins::PyModuleDef>
posixshmem::module_def(ctx),
pyexpat::module_def(ctx),
pystruct::module_def(ctx),
_queue::module_def(ctx),
random::module_def(ctx),
#[cfg(all(feature = "host_env", unix, not(target_os = "redox")))]
resource::module_def(ctx),

View File

@@ -537,7 +537,7 @@ impl PySet {
self.inner.len()
}
pub fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
self.inner.contains(needle, vm)
}
@@ -679,17 +679,17 @@ impl PySet {
}
#[pymethod]
pub fn discard(&self, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
fn discard(&self, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
self.inner.discard(&item, vm).map(|_| ())
}
#[pymethod]
pub fn clear(&self) {
fn clear(&self) {
self.inner.clear()
}
#[pymethod]
pub fn pop(&self, vm: &VirtualMachine) -> PyResult {
fn pop(&self, vm: &VirtualMachine) -> PyResult {
self.inner.pop(vm)
}
@@ -995,7 +995,7 @@ impl PyFrozenSet {
self.inner.len()
}
pub fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
self.inner.contains(needle, vm)
}

View File

@@ -42,22 +42,6 @@ impl Constructor for PyWeakProxy {
Self::Args { referent, callback }: Self::Args,
vm: &VirtualMachine,
) -> PyResult<Self> {
let weak = Self::new_weak(referent.as_ref(), callback.into_option(), vm)?;
// TODO: PyWeakProxy should use the same payload as PyWeak
Ok(Self { weak })
}
}
crate::common::static_cell! {
static WEAK_SUBCLASS: PyTypeRef;
}
impl PyWeakProxy {
fn new_weak(
referent: &PyObject,
callback: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<PyRef<PyWeak>> {
// using an internal subclass as the class prevents us from getting the generic weakref,
// which would mess up the weakref count
let weak_cls = WEAK_SUBCLASS.get_or_init(|| {
@@ -68,22 +52,15 @@ impl PyWeakProxy {
super::PyWeak::make_slots(),
)
});
referent.downgrade_with_typ(callback, weak_cls.clone(), vm)
// TODO: PyWeakProxy should use the same payload as PyWeak
Ok(Self {
weak: referent.downgrade_with_typ(callback.into_option(), weak_cls.clone(), vm)?,
})
}
}
pub fn new_weakproxy(
referent: &PyObject,
callback: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<PyRef<Self>> {
let weak = Self::new_weak(referent, callback, vm)?;
Ok(Self { weak }.into_ref(&vm.ctx))
}
#[must_use]
pub fn get_weak(&self) -> &PyRef<PyWeak> {
&self.weak
}
crate::common::static_cell! {
static WEAK_SUBCLASS: PyTypeRef;
}
#[pyclass(with(

View File

@@ -891,3 +891,13 @@ assert id(b) != id(b * 0)
assert id(b) != id(b * 1)
assert id(b) != id(1 * b)
assert id(b) != id(b * 2)
# Regression tests for isalpha/isalnum Unicode General Category correctness.
# These characters are in letter categories (Ll/Lo) and should return True,
# but were missed in older Unicode tables used by unic-ucd-category.
# See: https://github.com/RustPython/RustPython/pull/7520#issuecomment-4148322294
for _cp in [1376, 1416, 1519, 2160, 2161, 2162, 2163, 2164, 2165, 2166]:
_c = chr(_cp)
assert _c.isalpha(), f"U+{_cp:04X} should be isalpha"
assert _c.isalnum(), f"U+{_cp:04X} should be isalnum"

View File

@@ -11,6 +11,7 @@ c = ᚴ * 3
assert c == "👋👋👋"
import re
import unicodedata
assert unicodedata.category("a") == "Ll"
@@ -38,3 +39,10 @@ assert b"xn--pythn-mua.org.".decode("idna") == "pyth\xf6n.org."
# TODO: add east_asian_width and mirrored
# assert unicodedata.ucd_3_2_0.east_asian_width('\u231a') == 'N'
# assert not unicodedata.ucd_3_2_0.mirrored("\u0f3a")
# U+0345 COMBINING GREEK YPOGEGRAMMENI (category Mn) should not be alphanumeric.
# CPython's isalpha/isalnum use Unicode letter categories (Lu/Ll/Lt/Lm/Lo),
# not the broader Unicode Alphabetic derived property.
assert not "\u0345".isalpha(), "isalpha should not match Mn category characters"
assert not "\u0345".isalnum(), "isalnum should not match Mn category characters"
assert not re.match(r"\w", "\u0345"), r"\w should not match U+0345 (category Mn)"