Merge pull request #10766 from rdb/fix-10765

This commit is contained in:
Zac Hatfield-Dodds 2023-03-04 23:34:35 -08:00 committed by GitHub
commit b3b44ea814
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 35 additions and 28 deletions

View File

@ -0,0 +1 @@
Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.

View File

@ -1,4 +1,6 @@
"""Python version compatibility code.""" """Python version compatibility code."""
from __future__ import annotations
import dataclasses import dataclasses
import enum import enum
import functools import functools
@ -12,11 +14,8 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import Generic from typing import Generic
from typing import NoReturn from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union
import py import py
@ -46,7 +45,7 @@ LEGACY_PATH = py.path. local
# fmt: on # fmt: on
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH: def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances""" """Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path) return LEGACY_PATH(path)
@ -56,7 +55,7 @@ def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum): class NotSetType(enum.Enum):
token = 0 token = 0
NOTSET: "Final" = NotSetType.token # noqa: E305 NOTSET: Final = NotSetType.token # noqa: E305
# fmt: on # fmt: on
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
@ -94,7 +93,7 @@ def is_async_function(func: object) -> bool:
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
def getlocation(function, curdir: Optional[str] = None) -> str: def getlocation(function, curdir: str | None = None) -> str:
function = get_real_func(function) function = get_real_func(function)
fn = Path(inspect.getfile(function)) fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno lineno = function.__code__.co_firstlineno
@ -132,8 +131,8 @@ def getfuncargnames(
*, *,
name: str = "", name: str = "",
is_method: bool = False, is_method: bool = False,
cls: Optional[type] = None, cls: type | None = None,
) -> Tuple[str, ...]: ) -> tuple[str, ...]:
"""Return the names of a function's mandatory arguments. """Return the names of a function's mandatory arguments.
Should return the names of all function arguments that: Should return the names of all function arguments that:
@ -197,7 +196,7 @@ def getfuncargnames(
return arg_names return arg_names
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
# Note: this code intentionally mirrors the code at the beginning of # Note: this code intentionally mirrors the code at the beginning of
# getfuncargnames, to get the arguments which were excluded from its result # getfuncargnames, to get the arguments which were excluded from its result
# because they had default values. # because they had default values.
@ -228,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace") return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: Union[bytes, str]) -> str: def ascii_escaped(val: bytes | str) -> str:
r"""If val is pure ASCII, return it as an str, otherwise, escape r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes: bytes objects into a sequence of escaped bytes:
@ -355,7 +354,6 @@ else:
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
from functools import cached_property as cached_property from functools import cached_property as cached_property
else: else:
from typing import Type
class cached_property(Generic[_S, _T]): class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__") __slots__ = ("func", "__doc__")
@ -366,12 +364,12 @@ else:
@overload @overload
def __get__( def __get__(
self, instance: None, owner: Optional[Type[_S]] = ... self, instance: None, owner: type[_S] | None = ...
) -> "cached_property[_S, _T]": ) -> cached_property[_S, _T]:
... ...
@overload @overload
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T: def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
... ...
def __get__(self, instance, owner=None): def __get__(self, instance, owner=None):
@ -381,6 +379,18 @@ else:
return value return value
def get_user_id() -> int | None:
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
# win32 does not have a getuid() function.
# On Emscripten, getuid() is a stub that always returns 0.
if sys.platform in ("win32", "emscripten"):
return None
# getuid shouldn't fail, but cpython defines such a case.
# Let's hope for the best.
uid = os.getuid()
return uid if uid != -1 else None
# Perform exhaustiveness checking. # Perform exhaustiveness checking.
# #
# Consider this example: # Consider this example:

View File

@ -2,7 +2,6 @@
import dataclasses import dataclasses
import os import os
import re import re
import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from shutil import rmtree from shutil import rmtree
@ -30,7 +29,7 @@ from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf from .pathlib import rm_rf
from .pathlib import cleanup_dead_symlink from .pathlib import cleanup_dead_symlink
from _pytest.compat import final from _pytest.compat import final, get_user_id
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
@ -176,12 +175,9 @@ class TempPathFactory:
# Also, to keep things private, fixup any world-readable temp # Also, to keep things private, fixup any world-readable temp
# rootdir's permissions. Historically 0o755 was used, so we can't # rootdir's permissions. Historically 0o755 was used, so we can't
# just error out on this, at least for a while. # just error out on this, at least for a while.
if sys.platform != "win32": uid = get_user_id()
uid = os.getuid() if uid is not None:
rootdir_stat = rootdir.stat() rootdir_stat = rootdir.stat()
# getuid shouldn't fail, but cpython defines such a case.
# Let's hope for the best.
if uid != -1:
if rootdir_stat.st_uid != uid: if rootdir_stat.st_uid != uid:
raise OSError( raise OSError(
f"The temporary directory {rootdir} is not owned by the current user. " f"The temporary directory {rootdir} is not owned by the current user. "