<commit_before>""" support for providing temporary directories to test functions.  """
import os
import re
import tempfile
from typing import Optional

import attr
import py

import pytest
from .pathlib import ensure_reset_dir
from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import Path
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch

@attr.s
class TempPathFactory:
    """Factory for temporary directories under the common base temp directory.

    The base directory can be configured using the ``--basetemp`` option."""

    _given_basetemp = attr.ib(
        type=Path,
        # using os.path.abspath() to get absolute path instead of resolve() as it
        # does not work the same in all platforms (see #4427)
        # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
        # Ignore type because of https://github.com/python/mypy/issues/6172.
        converter=attr.converters.optional(
            lambda p: Path(os.path.abspath(str(p)))  # type: ignore
        ),
    )
    _trace = attr.ib()
    _basetemp = attr.ib(type=Optional[Path], default=None)

    @classmethod
    def from_config(cls, config) -> "TempPathFactory":
        """
        :param config: a pytest configuration
        """
        return cls(
            given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
        )

    def _ensure_relative_to_basetemp(self, basename: str):
        basename = os.path.normpath(basename)
        if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
            raise ValueError(
                "{} is not a normalized and relative path".format(basename)
            )
        return basename

    def mktemp(self, basename: str, numbered: bool = True) -> Path:
        """Creates a new temporary directory managed by the factory.

        :param basename:
            Directory base name, must be a relative path.

        :param numbered:
            If True, ensure the directory is unique by adding a number
            prefix greater than any existing one: ``basename="foo"`` and ``numbered=True``
            means that this function will create directories named ``"foo-0"``,
            ``"foo-1"``, ``"foo-2"`` and so on.

        :return:
            The path to the new directory.
        """
        basename = self._ensure_relative_to_basetemp(basename)
        if not numbered:
            p = self.getbasetemp().joinpath(basename)
            p.mkdir()
        else:
            p = make_numbered_dir(root=self.getbasetemp(), prefix=basename)
            self._trace("mktemp", p)
        return p

    def getbasetemp(self) -> Path:
        """ return base temporary directory. """
        if self._basetemp is not None:
            return self._basetemp

        if self._given_basetemp is not None:
            basetemp = self._given_basetemp
            ensure_reset_dir(basetemp)
            basetemp = basetemp.resolve()
        else:
            from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
            temproot = Path(from_env or tempfile.gettempdir()).resolve()
            user = get_user() or "unknown"
            # use a sub-directory in the temproot to speed-up
            # make_numbered_dir() call
            rootdir = temproot.joinpath("pytest-of-{}".format(user))
            rootdir.mkdir(exist_ok=True)
            basetemp = make_numbered_dir_with_cleanup(
                prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
            )
        assert basetemp is not None, basetemp
        self._basetemp = t = basetemp
        self._trace("new basetemp", t)
        return t

@attr.s
class TempdirFactory:
    """
    backward comptibility wrapper that implements
    :class:``py.path.local`` for :class:``TempPathFactory``
    """

    _tmppath_factory = attr.ib(type=TempPathFactory)

    def mktemp(self, basename: str, numbered: bool = True) -> py.path.local:
        """
        Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object.
        """
        return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())

    def getbasetemp(self):
        """backward compat wrapper for ``_tmppath_factory.getbasetemp``"""
        return py.path.local(self._tmppath_factory.getbasetemp().resolve())

def get_user() -> Optional[str]:
    """Return the current user name, or None if getuser() does not work
    in the current environment (see #1010).
    """
    import getpass

    try:
        return getpass.getuser()
    except (ImportError, KeyError):
        return None

def pytest_configure(config) -> None:
    """Create a TempdirFactory and attach it to the config object.

    This is to comply with existing plugins which expect the handler to be
    available at pytest_configure time, but ideally should be moved entirely
    to the tmpdir_factory session fixture.
    """
    mp = MonkeyPatch()
    tmppath_handler = TempPathFactory.from_config(config)
    t = TempdirFactory(tmppath_handler)
    config._cleanup.append(mp.undo)
    mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False)
    mp.setattr(config, "_tmpdirhandler", t, raising=False)

@pytest.fixture(scope="session")
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
    """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
    """
    # Set dynamically by pytest_configure() above.
    return request.config._tmpdirhandler  # type: ignore

@pytest.fixture(scope="session")
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
    """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
    """
    # Set dynamically by pytest_configure() above.
    return request.config._tmp_path_factory  # type: ignore

def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
    name = request.node.name
    name = re.sub(r"[\W]", "_", name)
    MAXVAL = 30
    name = name[:MAXVAL]
    return factory.mktemp(name, numbered=True)

@pytest.fixture
def tmpdir(tmp_path):
    """Return a temporary directory path object
    which is unique to each test function invocation,
    created as a sub directory of the base temporary
    directory.  The returned object is a `py.path.local`_
    path object.

    .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
    """
    return py.path.local(tmp_path)

@pytest.fixture
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
    """Return a temporary directory path object
    which is unique to each test function invocation,
    created as a sub directory of the base temporary
    directory.  The returned object is a :class:`pathlib.Path`
    object.

    .. note::

        in python < 3.6 this is a pathlib2.Path
    """

    return _mk_tmp(request, tmp_path_factory)
<commit_msg>tmpdir: clean up indirection via config for factories (#6767)<commit_after>""" support for providing temporary directories to test functions.  """
import os
import re
import tempfile
from typing import Optional

import attr
import py

import pytest
from .pathlib import ensure_reset_dir
from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import Path
from _pytest.fixtures import FixtureRequest

@attr.s
class TempPathFactory:
    """Factory for temporary directories under the common base temp directory.

    The base directory can be configured using the ``--basetemp`` option."""

    _given_basetemp = attr.ib(
        type=Path,
        # using os.path.abspath() to get absolute path instead of resolve() as it
        # does not work the same in all platforms (see #4427)
        # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
        # Ignore type because of https://github.com/python/mypy/issues/6172.
        converter=attr.converters.optional(
            lambda p: Path(os.path.abspath(str(p)))  # type: ignore
        ),
    )
    _trace = attr.ib()
    _basetemp = attr.ib(type=Optional[Path], default=None)

    @classmethod
    def from_config(cls, config) -> "TempPathFactory":
        """
        :param config: a pytest configuration
        """
        return cls(
            given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
        )

    def _ensure_relative_to_basetemp(self, basename: str):
        basename = os.path.normpath(basename)
        if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
            raise ValueError(
                "{} is not a normalized and relative path".format(basename)
            )
        return basename

    def mktemp(self, basename: str, numbered: bool = True) -> Path:
        """Creates a new temporary directory managed by the factory.

        :param basename:
            Directory base name, must be a relative path.

        :param numbered:
            If True, ensure the directory is unique by adding a number
            prefix greater than any existing one: ``basename="foo"`` and ``numbered=True``
            means that this function will create directories named ``"foo-0"``,
            ``"foo-1"``, ``"foo-2"`` and so on.

        :return:
            The path to the new directory.
        """
        basename = self._ensure_relative_to_basetemp(basename)
        if not numbered:
            p = self.getbasetemp().joinpath(basename)
            p.mkdir()
        else:
            p = make_numbered_dir(root=self.getbasetemp(), prefix=basename)
            self._trace("mktemp", p)
        return p

    def getbasetemp(self) -> Path:
        """ return base temporary directory. """
        if self._basetemp is not None:
            return self._basetemp

        if self._given_basetemp is not None:
            basetemp = self._given_basetemp
            ensure_reset_dir(basetemp)
            basetemp = basetemp.resolve()
        else:
            from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
            temproot = Path(from_env or tempfile.gettempdir()).resolve()
            user = get_user() or "unknown"
            # use a sub-directory in the temproot to speed-up
            # make_numbered_dir() call
            rootdir = temproot.joinpath("pytest-of-{}".format(user))
            rootdir.mkdir(exist_ok=True)
            basetemp = make_numbered_dir_with_cleanup(
                prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
            )
        assert basetemp is not None, basetemp
        self._basetemp = t = basetemp
        self._trace("new basetemp", t)
        return t

@attr.s
class TempdirFactory:
    """
    backward comptibility wrapper that implements
    :class:``py.path.local`` for :class:``TempPathFactory``
    """

    _tmppath_factory = attr.ib(type=TempPathFactory)

    def mktemp(self, basename: str, numbered: bool = True) -> py.path.local:
        """
        Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object.
        """
        return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())

    def getbasetemp(self):
        """backward compat wrapper for ``_tmppath_factory.getbasetemp``"""
        return py.path.local(self._tmppath_factory.getbasetemp().resolve())

def get_user() -> Optional[str]:
    """Return the current user name, or None if getuser() does not work
    in the current environment (see #1010).
    """
    import getpass

    try:
        return getpass.getuser()
    except (ImportError, KeyError):
        return None

@pytest.fixture(scope="session")
def tmpdir_factory(tmp_path_factory) -> TempdirFactory:
    """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
    """
    return TempdirFactory(tmp_path_factory)

@pytest.fixture(scope="session")
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
    """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
    """
    return TempPathFactory.from_config(request.config)

def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
    name = request.node.name
    name = re.sub(r"[\W]", "_", name)
    MAXVAL = 30
    name = name[:MAXVAL]
    return factory.mktemp(name, numbered=True)

@pytest.fixture
def tmpdir(tmp_path):
    """Return a temporary directory path object
    which is unique to each test function invocation,
    created as a sub directory of the base temporary
    directory.  The returned object is a `py.path.local`_
    path object.

    .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
    """
    return py.path.local(tmp_path)

@pytest.fixture
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
    """Return a temporary directory path object
    which is unique to each test function invocation,
    created as a sub directory of the base temporary
    directory.  The returned object is a :class:`pathlib.Path`
    object.

    .. note::

        in python < 3.6 this is a pathlib2.Path
    """

    return _mk_tmp(request, tmp_path_factory)
<|endoftext|>


<commit_before>"""Support for providing temporary directories to test functions."""
import os
import re
import sys
import tempfile
from pathlib import Path
from typing import Optional

import attr

from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf
from _pytest.compat import final
from _pytest.config import Config
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch

@final
@attr.s(init=False)
class TempPathFactory:
    """Factory for temporary directories under the common base temp directory.

    The base directory can be configured using the ``--basetemp`` option.
    """

    _given_basetemp = attr.ib(type=Optional[Path])
    _trace = attr.ib()
    _basetemp = attr.ib(type=Optional[Path])

    def __init__(
        self,
        given_basetemp: Optional[Path],
        trace,
        basetemp: Optional[Path] = None,
        *,
        _ispytest: bool = False,
    ) -> None:
        check_ispytest(_ispytest)
        if given_basetemp is None:
            self._given_basetemp = None
        else:
            # Use os.path.abspath() to get absolute path instead of resolve() as it
            # does not work the same in all platforms (see #4427).
            # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
            self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
        self._trace = trace
        self._basetemp = basetemp

    @classmethod
    def from_config(
        cls,
        config: Config,
        *,
        _ispytest: bool = False,
    ) -> "TempPathFactory":
        """Create a factory according to pytest configuration.

        :meta private:
        """
        check_ispytest(_ispytest)
        return cls(
            given_basetemp=config.option.basetemp,
            trace=config.trace.get("tmpdir"),
            _ispytest=True,
        )

    def _ensure_relative_to_basetemp(self, basename: str) -> str:
        basename = os.path.normpath(basename)
        if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
            raise ValueError(f"{basename} is not a normalized and relative path")
        return basename

    def mktemp(self, basename: str, numbered: bool = True) -> Path:
        """Create a new temporary directory managed by the factory.

        :param basename:
            Directory base name, must be a relative path.

        :param numbered:
            If ``True``, ensure the directory is unique by adding a numbered
            suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True``
            means that this function will create directories named ``"foo-0"``,
            ``"foo-1"``, ``"foo-2"`` and so on.

        :returns:
            The path to the new directory.
        """
        basename = self._ensure_relative_to_basetemp(basename)
        if not numbered:
            p = self.getbasetemp().joinpath(basename)
            p.mkdir(mode=0o700)
        else:
            p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
            self._trace("mktemp", p)
        return p

    def getbasetemp(self) -> Path:
        """Return the base temporary directory, creating it if needed."""
        if self._basetemp is not None:
            return self._basetemp

        if self._given_basetemp is not None:
            basetemp = self._given_basetemp
            if basetemp.exists():
                rm_rf(basetemp)
            basetemp.mkdir(mode=0o700)
            basetemp = basetemp.resolve()
        else:
            from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
            temproot = Path(from_env or tempfile.gettempdir()).resolve()
            user = get_user() or "unknown"
            # use a sub-directory in the temproot to speed-up
            # make_numbered_dir() call
            rootdir = temproot.joinpath(f"pytest-of-{user}")
            try:
                rootdir.mkdir(mode=0o700, exist_ok=True)
            except OSError:
                # getuser() likely returned illegal characters for the platform, use unknown back off mechanism
                rootdir = temproot.joinpath("pytest-of-unknown")
                rootdir.mkdir(mode=0o700, exist_ok=True)
            # Because we use exist_ok=True with a predictable name, make sure
            # we are the owners, to prevent any funny business (on unix, where
            # temproot is usually shared).
            # Also, to keep things private, fixup any world-readable temp
            # rootdir's permissions. Historically 0o755 was used, so we can't
            # just error out on this, at least for a while.
            if sys.platform != "win32":
                uid = os.getuid()
                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:
                        raise OSError(
                            f"The temporary directory {rootdir} is not owned by the current user. "
                            "Fix this and try again."
                        )
                    if (rootdir_stat.st_mode & 0o077) != 0:
                        os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
            basetemp = make_numbered_dir_with_cleanup(
                prefix="pytest-",
                root=rootdir,
                keep=3,
                lock_timeout=LOCK_TIMEOUT,
                mode=0o700,
            )
        assert basetemp is not None, basetemp
        self._basetemp = basetemp
        self._trace("new basetemp", basetemp)
        return basetemp

def get_user() -> Optional[str]:
    """Return the current user name, or None if getuser() does not work
    in the current environment (see #1010)."""
    import getpass

    try:
        return getpass.getuser()
    except (ImportError, KeyError):
        return None

def pytest_configure(config: Config) -> None:
    """Create a TempPathFactory and attach it to the config object.

    This is to comply with existing plugins which expect the handler to be
    available at pytest_configure time, but ideally should be moved entirely
    to the tmp_path_factory session fixture.
    """
    mp = MonkeyPatch()
    config.add_cleanup(mp.undo)
    _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
    mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)

@fixture(scope="session")
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
    """Return a :class:`pytest.TempPathFactory` instance for the test session."""
    # Set dynamically by pytest_configure() above.
    return request.config._tmp_path_factory  # type: ignore

def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
    name = request.node.name
    name = re.sub(r"[\W]", "_", name)
    MAXVAL = 30
    name = name[:MAXVAL]
    return factory.mktemp(name, numbered=True)

@fixture
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
    """Return a temporary directory path object which is unique to each test
    function invocation, created as a sub directory of the base temporary
    directory.

    By default, a new base temporary directory is created each test session,
    and old bases are removed after 3 sessions, to aid in debugging. If
    ``--basetemp`` is used then it is cleared each session. See :ref:`base
    temporary directory`.

    The returned object is a :class:`pathlib.Path` object.
    """

    return _mk_tmp(request, tmp_path_factory)
<commit_msg>fix: move 'import getpass' statement to try-clause<commit_after>"""Support for providing temporary directories to test functions."""
import os
import re
import sys
import tempfile
from pathlib import Path
from typing import Optional

import attr

from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf
from _pytest.compat import final
from _pytest.config import Config
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch

@final
@attr.s(init=False)
class TempPathFactory:
    """Factory for temporary directories under the common base temp directory.

    The base directory can be configured using the ``--basetemp`` option.
    """

    _given_basetemp = attr.ib(type=Optional[Path])
    _trace = attr.ib()
    _basetemp = attr.ib(type=Optional[Path])

    def __init__(
        self,
        given_basetemp: Optional[Path],
        trace,
        basetemp: Optional[Path] = None,
        *,
        _ispytest: bool = False,
    ) -> None:
        check_ispytest(_ispytest)
        if given_basetemp is None:
            self._given_basetemp = None
        else:
            # Use os.path.abspath() to get absolute path instead of resolve() as it
            # does not work the same in all platforms (see #4427).
            # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
            self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
        self._trace = trace
        self._basetemp = basetemp

    @classmethod
    def from_config(
        cls,
        config: Config,
        *,
        _ispytest: bool = False,
    ) -> "TempPathFactory":
        """Create a factory according to pytest configuration.

        :meta private:
        """
        check_ispytest(_ispytest)
        return cls(
            given_basetemp=config.option.basetemp,
            trace=config.trace.get("tmpdir"),
            _ispytest=True,
        )

    def _ensure_relative_to_basetemp(self, basename: str) -> str:
        basename = os.path.normpath(basename)
        if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
            raise ValueError(f"{basename} is not a normalized and relative path")
        return basename

    def mktemp(self, basename: str, numbered: bool = True) -> Path:
        """Create a new temporary directory managed by the factory.

        :param basename:
            Directory base name, must be a relative path.

        :param numbered:
            If ``True``, ensure the directory is unique by adding a numbered
            suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True``
            means that this function will create directories named ``"foo-0"``,
            ``"foo-1"``, ``"foo-2"`` and so on.

        :returns:
            The path to the new directory.
        """
        basename = self._ensure_relative_to_basetemp(basename)
        if not numbered:
            p = self.getbasetemp().joinpath(basename)
            p.mkdir(mode=0o700)
        else:
            p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
            self._trace("mktemp", p)
        return p

    def getbasetemp(self) -> Path:
        """Return the base temporary directory, creating it if needed."""
        if self._basetemp is not None:
            return self._basetemp

        if self._given_basetemp is not None:
            basetemp = self._given_basetemp
            if basetemp.exists():
                rm_rf(basetemp)
            basetemp.mkdir(mode=0o700)
            basetemp = basetemp.resolve()
        else:
            from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
            temproot = Path(from_env or tempfile.gettempdir()).resolve()
            user = get_user() or "unknown"
            # use a sub-directory in the temproot to speed-up
            # make_numbered_dir() call
            rootdir = temproot.joinpath(f"pytest-of-{user}")
            try:
                rootdir.mkdir(mode=0o700, exist_ok=True)
            except OSError:
                # getuser() likely returned illegal characters for the platform, use unknown back off mechanism
                rootdir = temproot.joinpath("pytest-of-unknown")
                rootdir.mkdir(mode=0o700, exist_ok=True)
            # Because we use exist_ok=True with a predictable name, make sure
            # we are the owners, to prevent any funny business (on unix, where
            # temproot is usually shared).
            # Also, to keep things private, fixup any world-readable temp
            # rootdir's permissions. Historically 0o755 was used, so we can't
            # just error out on this, at least for a while.
            if sys.platform != "win32":
                uid = os.getuid()
                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:
                        raise OSError(
                            f"The temporary directory {rootdir} is not owned by the current user. "
                            "Fix this and try again."
                        )
                    if (rootdir_stat.st_mode & 0o077) != 0:
                        os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
            basetemp = make_numbered_dir_with_cleanup(
                prefix="pytest-",
                root=rootdir,
                keep=3,
                lock_timeout=LOCK_TIMEOUT,
                mode=0o700,
            )
        assert basetemp is not None, basetemp
        self._basetemp = basetemp
        self._trace("new basetemp", basetemp)
        return basetemp

def get_user() -> Optional[str]:
    """Return the current user name, or None if getuser() does not work
    in the current environment (see #1010)."""
    try:
        # In some exotic environments, getpass may not be importable.
        import getpass
        return getpass.getuser()
    except (ImportError, KeyError):
        return None

def pytest_configure(config: Config) -> None:
    """Create a TempPathFactory and attach it to the config object.

    This is to comply with existing plugins which expect the handler to be
    available at pytest_configure time, but ideally should be moved entirely
    to the tmp_path_factory session fixture.
    """
    mp = MonkeyPatch()
    config.add_cleanup(mp.undo)
    _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
    mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)

@fixture(scope="session")
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
    """Return a :class:`pytest.TempPathFactory` instance for the test session."""
    # Set dynamically by pytest_configure() above.
    return request.config._tmp_path_factory  # type: ignore

def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
    name = request.node.name
    name = re.sub(r"[\W]", "_", name)
    MAXVAL = 30
    name = name[:MAXVAL]
    return factory.mktemp(name, numbered=True)

@fixture
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
    """Return a temporary directory path object which is unique to each test
    function invocation, created as a sub directory of the base temporary
    directory.

    By default, a new base temporary directory is created each test session,
    and old bases are removed after 3 sessions, to aid in debugging. If
    ``--basetemp`` is used then it is cleared each session. See :ref:`base
    temporary directory`.

    The returned object is a :class:`pathlib.Path` object.
    """

    return _mk_tmp(request, tmp_path_factory)
<|endoftext|>


<commit_before>
import discord
from discord import app_commands
from discord.ext import commands, menus

import io
import textwrap
from typing import Optional
import urllib.error

from bs4 import BeautifulSoup
from google.api_core.exceptions import InvalidArgument
import spellchecker

from utilities import checks
from utilities.paginators import ButtonPaginator

async def setup(bot):
    await bot.add_cog(Words())

class Words(commands.Cog):

    async def cog_check(self, ctx):
        return await checks.not_forbidden().predicate(ctx)

    @commands.hybrid_command(aliases = ["antonym"])
    async def antonyms(self, ctx, word: str):
        """
        Antonyms of a word

        Parameters
        ----------
        word
            Word to get antonyms for
        """
        await ctx.defer()

        try:
            antonyms = ctx.bot.wordnik_word_api.getRelatedWords(
                word, relationshipTypes = "antonym",
                useCanonical = "true", limitPerRelationshipType = 100
            )
        except urllib.error.HTTPError as e:
            if e.code == 404:
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Word or antonyms not found"
                )
                return
            raise

        if not antonyms:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Word or antonyms not found"
            )
            return

        await ctx.embed_reply(
            title = f"Antonyms of {word.capitalize()}",
            description = ", ".join(antonyms[0].words)
        )

    @commands.hybrid_command(
        aliases = ["definition", "definitions", "dictionary"]
    )
    async def define(self, ctx, word: str):
        """
        Define a word

        Parameters
        ----------
        word
            Word to define
        """
        await ctx.defer()

        try:
            definitions = ctx.bot.wordnik_word_api.getDefinitions(word)
            # useCanonical = True ?
        except urllib.error.HTTPError as e:
            if e.code in (404, 429):
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Error: {e.reason}"
                )
                return
            raise

        definitions = [
            definition for definition in definitions if definition.text
        ]

        if not definitions:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Definition not found"
            )

        paginator = ButtonPaginator(ctx, DefineSource(definitions))
        await paginator.start()
        ctx.bot.views.append(paginator)

    @commands.hybrid_command(aliases = ["audiodefine", "pronunciation"])
    async def pronounce(self, ctx, word: str):
        """
        Pronunciation of a word

        Parameters
        ----------
        word
            Word to get pronunciation for
        """
        await ctx.defer()

        try:
            pronunciation = ctx.bot.wordnik_word_api.getTextPronunciations(
                word, limit = 1
            )
        except urllib.error.HTTPError as e:
            if e.code == 404:
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Word or pronunciation not found"
                )
                return
            raise

        description = (
            pronunciation[0].raw.strip("()")
            if pronunciation else "Audio File Link"
        )

        try:
            audio_file = ctx.bot.wordnik_word_api.getAudio(word, limit = 1)
        except urllib.error.HTTPError as e:
            if e.code == 404:
                audio_file = None
            else:
                raise

        file = None
        if audio_file:
            file_url = audio_file[0].fileUrl
            async with ctx.bot.aiohttp_session.get(file_url) as resp:
                data = await resp.read()

            file = discord.File(
                io.BytesIO(data), filename = f"Pronunciation_of_{word}.mp3"
            )
        elif not pronunciation:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Word or pronunciation not found"
            )
            return

        await ctx.embed_reply(
            title = f"Pronunciation of \"{word}\"",
            description = description,
            file = file
        )

    @commands.hybrid_command(aliases = ["rhyme"])
    async def rhymes(self, ctx, word: str):
        """
        Rhymes of a word

        Paramters
        ---------
        word
            Word to get rhymes for
        """
        await ctx.defer()

        try:
            rhymes = ctx.bot.wordnik_word_api.getRelatedWords(
                word, relationshipTypes = "rhyme",
                limitPerRelationshipType = 100
            )
        except urllib.error.HTTPError as e:
            if e.code == 404:
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Word or rhymes not found"
                )
                return
            raise

        if not rhymes:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Word or rhymes not found"
            )
            return

        await ctx.embed_reply(
            title = f"Words that rhyme with {word.capitalize()}",
            description = ", ".join(rhymes[0].words)
        )

    @commands.command()
    async def spellcheck(self, ctx, *words: str):
        '''Check the spelling of words'''
        checker = spellchecker.SpellChecker()
        if len(words) == 1:
            await ctx.embed_reply(", ".join(checker.candidates(words[0])))
        else:
            await ctx.embed_reply(
                ' '.join(checker.correction(word) for word in words)
            )

    @commands.hybrid_command(aliases = ["synonym"])
    async def synonyms(self, ctx, word: str):
        """
        Synonyms of a word

        Parameters
        ----------
        word
            Word to get synonyms for
        """
        await ctx.defer()

        try:
            synonyms = ctx.bot.wordnik_word_api.getRelatedWords(
                word, relationshipTypes = "synonym",
                useCanonical = "true", limitPerRelationshipType = 100
            )
        except urllib.error.HTTPError as e:
            if e.code == 404:
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Word or synonyms not found"
                )
                return
            raise

        if not synonyms:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Word or synonyms not found"
            )
            return

        await ctx.embed_reply(
            title = f"Synonyms of {word.capitalize()}",
            description = ", ".join(synonyms[0].words)
        )

    @commands.group(case_insensitive = True, invoke_without_command = True)
    async def translate(self, ctx, *, text: Optional[str]):
        '''Translate to English'''
        # TODO: From and to language code options?
        if not text:
            if ctx.message.reference:
                referenced_message = (
                    ctx.message.reference.cached_message or
                    await ctx.channel.fetch_message(
                        ctx.message.reference.message_id
                    )
                )
                text = referenced_message.content
            else:
                await ctx.send_help(ctx.command)
                return

        response = await ctx.bot.google_cloud_translation_service_client.translate_text(
            contents = [text],
            mime_type = "text/plain",
            parent = f"projects/{ctx.bot.google_cloud_project_id}/locations/global",
            target_language_code = "en"
        )
        translation = response.translations[0]

        await ctx.embed_reply(
            translation.translated_text,
            footer_text = f"Detected Language Code: {translation.detected_language_code}",
            reference = ctx.message.reference if not text else None,
            mention_author = (
                referenced_message.author in ctx.message.mentions
                if ctx.message.reference and not text else None
            )
        )

    @translate.command(name = "from")
    async def translate_from(
        self, ctx, from_language_code: str, to_language_code: str, *,
        text: str
    ):
        '''Translate from a specific language to another'''
        # TODO: Default to_language_code?
        try:
            response = await ctx.bot.google_cloud_translation_service_client.translate_text(
                contents = [text],
                mime_type = "text/plain",
                parent = f"projects/{ctx.bot.google_cloud_project_id}/locations/global",
                source_language_code = from_language_code,
                target_language_code = to_language_code
            )
        except InvalidArgument as e:
            await ctx.embed_reply(f"{ctx.bot.error_emoji} Error: {e}")
            return
        await ctx.embed_reply(response.translations[0].translated_text)

    @translate.command(
        name = "languages", aliases = ["codes", "language_codes"]
    )
    async def translate_languages(self, ctx, language_code: str = "en"):
        '''Language Codes'''
        response = await ctx.bot.google_cloud_translation_service_client.get_supported_languages(
            display_language_code = language_code,
            parent = f"projects/{ctx.bot.google_cloud_project_id}/locations/global",
        )
        await ctx.embed_reply(
            ", ".join(
                f"{language.display_name} ({language.language_code})"
                for language in response.languages
            )
        )

    @translate.command(name = "to")
    async def translate_to(
        self, ctx, language_code: str, *, text: Optional[str]
    ):
        '''Translate to a specific language'''
        if not text:
            if ctx.message.reference:
                referenced_message = (
                    ctx.message.reference.cached_message or
                    await ctx.channel.fetch_message(
                        ctx.message.reference.message_id
                    )
                )
                text = referenced_message.content
            else:
                await ctx.send_help(ctx.command)
                return

        response = await ctx.bot.google_cloud_translation_service_client.translate_text(
            contents = [text],
            mime_type = "text/plain",
            parent = f"projects/{ctx.bot.google_cloud_project_id}/locations/global",
            target_language_code = language_code
        )
        translation = response.translations[0]

        await ctx.embed_reply(
            translation.translated_text,
            footer_text = f"Detected Language Code: {translation.detected_language_code}",
            reference = ctx.message.reference if not text else None,
            mention_author = (
                referenced_message.author in ctx.message.mentions
                if ctx.message.reference and not text else None
            )
        )

    @commands.group(
        aliases = [
            "urband", "urban_dictionary", "urbandefine", "urban_define"
        ], case_insensitive = True, invoke_without_command = True
    )
    async def urbandictionary(self, ctx, *, term: str):
        '''Urban Dictionary'''
        # TODO: Convert to define/dictionary subcommand urban and add urband
        # etc. as command aliases
        url = "http://api.urbandictionary.com/v0/define"
        params = {"term": term}
        async with ctx.bot.aiohttp_session.get(url, params = params) as resp:
            data = await resp.json()

        if not (definitions := data.get("list")):
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} No results found"
            )
            return

        paginator = ButtonPaginator(ctx, UrbanDictionarySource(definitions))
        await paginator.start()
        ctx.bot.views.append(paginator)

class DefineSource(menus.ListPageSource):

    def __init__(self, definitions):
        super().__init__(definitions, per_page = 1)

    async def format_page(self, menu, definition):
        kwargs = {}

        embed = discord.Embed(
            title = definition.word,
            description = BeautifulSoup(
                definition.text, "html.parser"
            ).get_text(),
            color = menu.bot.bot_color
        ).set_footer(
            text = definition.attributionText
        )

        if isinstance(menu.ctx_or_interaction, commands.Context):
            if not menu.ctx_or_interaction.interaction:
                embed.set_author(
                    name = menu.ctx.author.display_name,
                    icon_url = menu.ctx.author.display_avatar.url
                )
                kwargs["content"] = (
                    f"In response to: `{menu.ctx.message.clean_content}`"
                )
        elif not isinstance(menu.ctx_or_interaction, discord.Interaction):
            raise RuntimeError(
                "DefineSource using neither Context nor Interaction"
            )

        kwargs["embed"] = embed

        return kwargs

class UrbanDictionarySource(menus.ListPageSource):

    def __init__(self, definitions):
        super().__init__(definitions, per_page = 1)

    async def format_page(self, menu, definition):
        votes = (
            f"\N{THUMBS UP SIGN}{menu.ctx.bot.emoji_skin_tone} {definition['thumbs_up']}"
            " | "
            f"\N{THUMBS DOWN SIGN}{menu.ctx.bot.emoji_skin_tone} {definition['thumbs_down']}"
        )
        return {
            "content": f"In response to: `{menu.ctx.message.clean_content}`",
            "embed": discord.Embed(
                title = definition["word"], url = definition["permalink"],
                description = definition["definition"],
                color = menu.ctx.bot.bot_color
            ).set_author(
                name = menu.ctx.author.display_name,
                icon_url = menu.ctx.author.display_avatar.url
            ).add_field(
                name = "Example",
                value = (
                    textwrap.shorten(
                        definition["example"],
                        width = menu.ctx.bot.EFVCL - len(votes) - 2,
                        # EFVCL: Embed Field Value Character Limit
                        placeholder = "..."
                    ) + "\n\n" + votes
                )
            )
        }
        # TODO: Check description/definition length?

<commit_msg>[Discord] Remove now unnecessary import for Words cog<commit_after>
import discord
from discord.ext import commands, menus

import io
import textwrap
from typing import Optional
import urllib.error

from bs4 import BeautifulSoup
from google.api_core.exceptions import InvalidArgument
import spellchecker

from utilities import checks
from utilities.paginators import ButtonPaginator

async def setup(bot):
    await bot.add_cog(Words())

class Words(commands.Cog):

    async def cog_check(self, ctx):
        return await checks.not_forbidden().predicate(ctx)

    @commands.hybrid_command(aliases = ["antonym"])
    async def antonyms(self, ctx, word: str):
        """
        Antonyms of a word

        Parameters
        ----------
        word
            Word to get antonyms for
        """
        await ctx.defer()

        try:
            antonyms = ctx.bot.wordnik_word_api.getRelatedWords(
                word, relationshipTypes = "antonym",
                useCanonical = "true", limitPerRelationshipType = 100
            )
        except urllib.error.HTTPError as e:
            if e.code == 404:
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Word or antonyms not found"
                )
                return
            raise

        if not antonyms:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Word or antonyms not found"
            )
            return

        await ctx.embed_reply(
            title = f"Antonyms of {word.capitalize()}",
            description = ", ".join(antonyms[0].words)
        )

    @commands.hybrid_command(
        aliases = ["definition", "definitions", "dictionary"]
    )
    async def define(self, ctx, word: str):
        """
        Define a word

        Parameters
        ----------
        word
            Word to define
        """
        await ctx.defer()

        try:
            definitions = ctx.bot.wordnik_word_api.getDefinitions(word)
            # useCanonical = True ?
        except urllib.error.HTTPError as e:
            if e.code in (404, 429):
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Error: {e.reason}"
                )
                return
            raise

        definitions = [
            definition for definition in definitions if definition.text
        ]

        if not definitions:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Definition not found"
            )

        paginator = ButtonPaginator(ctx, DefineSource(definitions))
        await paginator.start()
        ctx.bot.views.append(paginator)

    @commands.hybrid_command(aliases = ["audiodefine", "pronunciation"])
    async def pronounce(self, ctx, word: str):
        """
        Pronunciation of a word

        Parameters
        ----------
        word
            Word to get pronunciation for
        """
        await ctx.defer()

        try:
            pronunciation = ctx.bot.wordnik_word_api.getTextPronunciations(
                word, limit = 1
            )
        except urllib.error.HTTPError as e:
            if e.code == 404:
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Word or pronunciation not found"
                )
                return
            raise

        description = (
            pronunciation[0].raw.strip("()")
            if pronunciation else "Audio File Link"
        )

        try:
            audio_file = ctx.bot.wordnik_word_api.getAudio(word, limit = 1)
        except urllib.error.HTTPError as e:
            if e.code == 404:
                audio_file = None
            else:
                raise

        file = None
        if audio_file:
            file_url = audio_file[0].fileUrl
            async with ctx.bot.aiohttp_session.get(file_url) as resp:
                data = await resp.read()

            file = discord.File(
                io.BytesIO(data), filename = f"Pronunciation_of_{word}.mp3"
            )
        elif not pronunciation:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Word or pronunciation not found"
            )
            return

        await ctx.embed_reply(
            title = f"Pronunciation of \"{word}\"",
            description = description,
            file = file
        )

    @commands.hybrid_command(aliases = ["rhyme"])
    async def rhymes(self, ctx, word: str):
        """
        Rhymes of a word

        Paramters
        ---------
        word
            Word to get rhymes for
        """
        await ctx.defer()

        try:
            rhymes = ctx.bot.wordnik_word_api.getRelatedWords(
                word, relationshipTypes = "rhyme",
                limitPerRelationshipType = 100
            )
        except urllib.error.HTTPError as e:
            if e.code == 404:
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Word or rhymes not found"
                )
                return
            raise

        if not rhymes:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Word or rhymes not found"
            )
            return

        await ctx.embed_reply(
            title = f"Words that rhyme with {word.capitalize()}",
            description = ", ".join(rhymes[0].words)
        )

    @commands.command()
    async def spellcheck(self, ctx, *words: str):
        '''Check the spelling of words'''
        checker = spellchecker.SpellChecker()
        if len(words) == 1:
            await ctx.embed_reply(", ".join(checker.candidates(words[0])))
        else:
            await ctx.embed_reply(
                ' '.join(checker.correction(word) for word in words)
            )

    @commands.hybrid_command(aliases = ["synonym"])
    async def synonyms(self, ctx, word: str):
        """
        Synonyms of a word

        Parameters
        ----------
        word
            Word to get synonyms for
        """
        await ctx.defer()

        try:
            synonyms = ctx.bot.wordnik_word_api.getRelatedWords(
                word, relationshipTypes = "synonym",
                useCanonical = "true", limitPerRelationshipType = 100
            )
        except urllib.error.HTTPError as e:
            if e.code == 404:
                await ctx.embed_reply(
                    f"{ctx.bot.error_emoji} Word or synonyms not found"
                )
                return
            raise

        if not synonyms:
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} Word or synonyms not found"
            )
            return

        await ctx.embed_reply(
            title = f"Synonyms of {word.capitalize()}",
            description = ", ".join(synonyms[0].words)
        )

    @commands.group(case_insensitive = True, invoke_without_command = True)
    async def translate(self, ctx, *, text: Optional[str]):
        '''Translate to English'''
        # TODO: From and to language code options?
        if not text:
            if ctx.message.reference:
                referenced_message = (
                    ctx.message.reference.cached_message or
                    await ctx.channel.fetch_message(
                        ctx.message.reference.message_id
                    )
                )
                text = referenced_message.content
            else:
                await ctx.send_help(ctx.command)
                return

        response = await ctx.bot.google_cloud_translation_service_client.translate_text(
            contents = [text],
            mime_type = "text/plain",
            parent = f"projects/{ctx.bot.google_cloud_project_id}/locations/global",
            target_language_code = "en"
        )
        translation = response.translations[0]

        await ctx.embed_reply(
            translation.translated_text,
            footer_text = f"Detected Language Code: {translation.detected_language_code}",
            reference = ctx.message.reference if not text else None,
            mention_author = (
                referenced_message.author in ctx.message.mentions
                if ctx.message.reference and not text else None
            )
        )

    @translate.command(name = "from")
    async def translate_from(
        self, ctx, from_language_code: str, to_language_code: str, *,
        text: str
    ):
        '''Translate from a specific language to another'''
        # TODO: Default to_language_code?
        try:
            response = await ctx.bot.google_cloud_translation_service_client.translate_text(
                contents = [text],
                mime_type = "text/plain",
                parent = f"projects/{ctx.bot.google_cloud_project_id}/locations/global",
                source_language_code = from_language_code,
                target_language_code = to_language_code
            )
        except InvalidArgument as e:
            await ctx.embed_reply(f"{ctx.bot.error_emoji} Error: {e}")
            return
        await ctx.embed_reply(response.translations[0].translated_text)

    @translate.command(
        name = "languages", aliases = ["codes", "language_codes"]
    )
    async def translate_languages(self, ctx, language_code: str = "en"):
        '''Language Codes'''
        response = await ctx.bot.google_cloud_translation_service_client.get_supported_languages(
            display_language_code = language_code,
            parent = f"projects/{ctx.bot.google_cloud_project_id}/locations/global",
        )
        await ctx.embed_reply(
            ", ".join(
                f"{language.display_name} ({language.language_code})"
                for language in response.languages
            )
        )

    @translate.command(name = "to")
    async def translate_to(
        self, ctx, language_code: str, *, text: Optional[str]
    ):
        '''Translate to a specific language'''
        if not text:
            if ctx.message.reference:
                referenced_message = (
                    ctx.message.reference.cached_message or
                    await ctx.channel.fetch_message(
                        ctx.message.reference.message_id
                    )
                )
                text = referenced_message.content
            else:
                await ctx.send_help(ctx.command)
                return

        response = await ctx.bot.google_cloud_translation_service_client.translate_text(
            contents = [text],
            mime_type = "text/plain",
            parent = f"projects/{ctx.bot.google_cloud_project_id}/locations/global",
            target_language_code = language_code
        )
        translation = response.translations[0]

        await ctx.embed_reply(
            translation.translated_text,
            footer_text = f"Detected Language Code: {translation.detected_language_code}",
            reference = ctx.message.reference if not text else None,
            mention_author = (
                referenced_message.author in ctx.message.mentions
                if ctx.message.reference and not text else None
            )
        )

    @commands.group(
        aliases = [
            "urband", "urban_dictionary", "urbandefine", "urban_define"
        ], case_insensitive = True, invoke_without_command = True
    )
    async def urbandictionary(self, ctx, *, term: str):
        '''Urban Dictionary'''
        # TODO: Convert to define/dictionary subcommand urban and add urband
        # etc. as command aliases
        url = "http://api.urbandictionary.com/v0/define"
        params = {"term": term}
        async with ctx.bot.aiohttp_session.get(url, params = params) as resp:
            data = await resp.json()

        if not (definitions := data.get("list")):
            await ctx.embed_reply(
                f"{ctx.bot.error_emoji} No results found"
            )
            return

        paginator = ButtonPaginator(ctx, UrbanDictionarySource(definitions))
        await paginator.start()
        ctx.bot.views.append(paginator)

class DefineSource(menus.ListPageSource):

    def __init__(self, definitions):
        super().__init__(definitions, per_page = 1)

    async def format_page(self, menu, definition):
        kwargs = {}

        embed = discord.Embed(
            title = definition.word,
            description = BeautifulSoup(
                definition.text, "html.parser"
            ).get_text(),
            color = menu.bot.bot_color
        ).set_footer(
            text = definition.attributionText
        )

        if isinstance(menu.ctx_or_interaction, commands.Context):
            if not menu.ctx_or_interaction.interaction:
                embed.set_author(
                    name = menu.ctx.author.display_name,
                    icon_url = menu.ctx.author.display_avatar.url
                )
                kwargs["content"] = (
                    f"In response to: `{menu.ctx.message.clean_content}`"
                )
        elif not isinstance(menu.ctx_or_interaction, discord.Interaction):
            raise RuntimeError(
                "DefineSource using neither Context nor Interaction"
            )

        kwargs["embed"] = embed

        return kwargs

class UrbanDictionarySource(menus.ListPageSource):

    def __init__(self, definitions):
        super().__init__(definitions, per_page = 1)

    async def format_page(self, menu, definition):
        votes = (
            f"\N{THUMBS UP SIGN}{menu.ctx.bot.emoji_skin_tone} {definition['thumbs_up']}"
            " | "
            f"\N{THUMBS DOWN SIGN}{menu.ctx.bot.emoji_skin_tone} {definition['thumbs_down']}"
        )
        return {
            "content": f"In response to: `{menu.ctx.message.clean_content}`",
            "embed": discord.Embed(
                title = definition["word"], url = definition["permalink"],
                description = definition["definition"],
                color = menu.ctx.bot.bot_color
            ).set_author(
                name = menu.ctx.author.display_name,
                icon_url = menu.ctx.author.display_avatar.url
            ).add_field(
                name = "Example",
                value = (
                    textwrap.shorten(
                        definition["example"],
                        width = menu.ctx.bot.EFVCL - len(votes) - 2,
                        # EFVCL: Embed Field Value Character Limit
                        placeholder = "..."
                    ) + "\n\n" + votes
                )
            )
        }
        # TODO: Check description/definition length?

<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
#
# Cilium documentation build configuration file, created by
# sphinx-quickstart on Sun Feb 12 18:34:43 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))

# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.ifconfig',
    'sphinx.ext.githubpages',
    'sphinxcontrib.openapi']

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = u'Cilium'
copyright = u'2017, Cilium Authors'
author = u'Cilium Authors'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'v0.8'
# The full version, including alpha/beta/rc tags.
release = u'v0.8'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False

# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
html_theme_path = ["_themes", ]

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['images']

# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = 'Ciliumdoc'

# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',

    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',

    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',

    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
    'extraclassoptions': 'openany',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (master_doc, 'Cilium.tex', u'Cilium Documentation',
     u'Cilium Authors', 'manual'),
]

# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (master_doc, 'cilium', u'Cilium Documentation',
     [author], 1)
]

# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'Cilium', u'Cilium Documentation',
     author, 'Cilium', 'One line description of project.',
     'Miscellaneous'),
]

<commit_msg>Documentation: use git to set version info<commit_after># -*- coding: utf-8 -*-
#
# Cilium documentation build configuration file, created by
# sphinx-quickstart on Sun Feb 12 18:34:43 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
# import sys
import re
# sys.path.insert(0, os.path.abspath('.'))

# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.ifconfig',
    'sphinx.ext.githubpages',
    'sphinxcontrib.openapi']

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = u'Cilium'
copyright = u'2017, Cilium Authors'
author = u'Cilium Authors'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
release = re.sub('^v', '', os.popen('git describe --tags').read().strip())
# The full version, including alpha/beta/rc tags.
version = release

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False

# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
html_theme_path = ["_themes", ]

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['images']

# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = 'Ciliumdoc'

# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',

    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',

    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',

    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
    'extraclassoptions': 'openany',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (master_doc, 'Cilium.tex', u'Cilium Documentation',
     u'Cilium Authors', 'manual'),
]

# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (master_doc, 'cilium', u'Cilium Documentation',
     [author], 1)
]

# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'Cilium', u'Cilium Documentation',
     author, 'Cilium', 'One line description of project.',
     'Miscellaneous'),
]

<|endoftext|>


<commit_before>import threading
import socket
import re
import os
import time

status_codes = {200:'OK',
				400:'Bad request',
				404:'Not found',
				405:'Method not allowed', # not currently used!
				408:'Request timeout'}
FIRST_LINE_OK = 0
TIMED_OUT = 1
LINE_TOO_LONG = 2

MAX_WAIT_TIME = 10.0

class BadRequestException(Exception):
	pass

def socket_lines(sock, wait_time=MAX_WAIT_TIME):
	""" Adapted from Aaron Watters at stackoverflow """
	buff = ''
	buffering = True
	start_time = time.time()
	while buffering or buff:
		if buffering:
			elapsed_time = time.time() - start_time
			if elapsed_time > wait_time:
				raise socket.timeout
			sock.settimeout(wait_time - elapsed_time)
			new_input = sock.recv(4096)

			if new_input:
				buff += new_input
			else:
				buffering = False

		if '\r\n' in buff:
			line, buff = buff.split('\r\n', 1)
			yield line+'\r\n'
		elif not buffering:
			yield buff

class HTTP_Responder(object):
	def __init__(self, client_socket):
		self.socket = client_socket

	def reply_invalid_request(self):
		"""Sends a 400 bad request"""
		self.send_reply(400)

	def reply_invalid_file(self):
		"""Sends a 404 Not found message"""
		self.send_reply(404)

	def reply_invalid_method(self):
		"""Sends a 405 method not allowed"""
		self.send_reply(405)

	def reply_timed_out(self):
		"""Sends a 408 client timed out message"""
		self.send_reply(408)

	def reply_file(self, f):
		"""Sends a 200 OK, and a file object"""
		# Not good for very large files. It reads the whole object
		# before sending it, instead of read-send-read-send.
		content = f.read()
		f.close()
		# lookup file extension
		extention = f.name.split('.')[-1] # file extension
		extensions = {'jpg':'image/jpeg', 'txt':'text/plain'}
		contentType = extensions.get(extention, None)
		self.send_reply(200, content, contentType=contentType)

	def send_reply(self, status, content=None, contentType=None):
		response = 'HTTP/1.1 {} {}\r\n'.format(status, status_codes[status])
		if not content:
			content = status_codes[status]

		if contentType:
			response += 'Content-Type: {}\r\n'.format(contentType)
		response += 'Content-Length: {}\r\n\r\n'.format(len(content))

		response += content

		try:
			self.socket.sendall(response)
		except:
			print('An error occurred while sending the reply.')
			pass
		finally:
			self.socket.close()

class Base_Request_handler(object):
	def __init__(self, method, path, contents_generator, sock):
		self.method = method
		self.path = path
		self.contents_generator = contents_generator
		self.socket = sock

class Static_handler(Base_Request_handler):
	def handle_request(self):
		responder = HTTP_Responder(self.socket)
		path = 'www'+self.path
		try:
			f = open(path)
		except IOError:
			responder.reply_invalid_file()
			return

		responder.reply_file(f)

class CGI_handler(Base_Request_handler):
	pass

class HTTP_handler(object):
	def __init__(self, client_socket):
		self.socket = client_socket

	def method_and_path_from_line(self, first_line):
		try:
			line, _ = first_line.split('\r\n')
			method, path, _ = line.split(' ', 2)
			path = self.parse_and_normalize_path(path)
			return method, path
		except ValueError:
			raise BadRequestException
		except:
			raise

	def parse_and_normalize_path(self, path):
		p = re.compile('^/([\w\-.~/]|%[0-9A-Fa-f]{2})*$')
		if not p.match(path):
			raise BadRequestException

		def percent_decode(m):
			s = m.group(1)
			return s[1:].decode('hex')

		decoded_path = re.sub('(%[0-9A-Fa-f]{2})', percent_decode, path)
		normalized_path = os.path.normpath(decoded_path)

		return normalized_path

	def route_request(self):
		""" Parses the first line, and hands off the connection to either
			the static file handler or the cgi handler. """

		socket_lines_gen = socket_lines(self.socket)

		try:
			first_line = socket_lines_gen.next()
		except socket.timeout:
			HTTP_Responder(self.socket).reply_timed_out()
			return

		try:
			method, path = self.method_and_path_from_line(first_line)
		except BadRequestException:
			HTTP_Responder(self.socket).reply_invalid_request()
			return

		if path.startswith('/cgi-bin/'):
			handler = CGI_handler(method, path, socket_lines_gen, self.socket)
		else:
			handler = Static_handler(method, path, socket_lines_gen, self.socket)

		handler.handle_request()

class Handler_thread(threading.Thread):
	def run(self, client_socket):
		new_handler = HTTP_handler(client_socket)
		new_handler.route_request()<commit_msg>Refactoring<commit_after>import threading
import socket
import re
import os
import time
import subprocess

status_codes = {200:'OK',
				400:'Bad request',
				404:'Not found',
				405:'Method not allowed', # not currently used!
				408:'Request timeout'}
FIRST_LINE_OK = 0
TIMED_OUT = 1
LINE_TOO_LONG = 2

MAX_WAIT_TIME = 10.0

class BadRequestException(Exception):
	pass

def socket_lines(sock, wait_time=MAX_WAIT_TIME):
	""" Adapted from Aaron Watters at stackoverflow """
	buff = ''
	buffering = True
	start_time = time.time()
	while buffering or buff:
		if buffering:
			elapsed_time = time.time() - start_time
			if elapsed_time > wait_time:
				raise socket.timeout
			sock.settimeout(wait_time - elapsed_time)
			new_input = sock.recv(4096)

			if new_input:
				buff += new_input
			else:
				buffering = False

		if '\r\n' in buff:
			line, buff = buff.split('\r\n', 1)
			yield line+'\r\n'
		elif not buffering:
			yield buff

class HTTP_Responder(object):
	def __init__(self, client_socket):
		self.socket = client_socket

	def reply_invalid_request(self):
		"""Sends a 400 bad request"""
		self.send_reply(400)

	def reply_invalid_file(self):
		"""Sends a 404 Not found message"""
		self.send_reply(404)

	def reply_invalid_method(self):
		"""Sends a 405 method not allowed"""
		self.send_reply(405)

	def reply_timed_out(self):
		"""Sends a 408 client timed out message"""
		self.send_reply(408)

	def reply_file(self, f):
		"""Sends a 200 OK, and a file object"""
		# Not good for very large files. It reads the whole object
		# before sending it, instead of read-send-read-send.
		content = f.read()
		f.close()
		# lookup file extension
		extention = f.name.split('.')[-1] # file extension
		extensions = {'jpg':'image/jpeg', 'txt':'text/plain'}
		contentType = extensions.get(extention, None)
		self.send_reply(200, content, contentType=contentType)

	def send_reply(self, status, content=None, contentType=None):
		response = 'HTTP/1.1 {} {}\r\n'.format(status, status_codes[status])
		if not content:
			content = status_codes[status]

		if contentType:
			response += 'Content-Type: {}\r\n'.format(contentType)
		response += 'Content-Length: {}\r\n\r\n'.format(len(content))

		response += content

		try:
			self.socket.sendall(response)
		except:
			print('An error occurred while sending the reply.')
			pass
		finally:
			self.socket.close()

class Base_Request_handler(object):
	def __init__(self, method, path, headers, body, sock):
		self.method = method
		self.path = path
		self.headers = headers
		self.body = body
		#self.contents_generator = contents_generator
		self.socket = sock

class Static_handler(Base_Request_handler):
	def handle_request(self):
		responder = HTTP_Responder(self.socket)
		path = 'www'+self.path

		if self.method != 'GET':
			responder.reply_invalid_method()
			return

		try:
			f = open(path)
		except IOError:
			responder.reply_invalid_file()
			return

		responder.reply_file(f)

class CGI_handler(Base_Request_handler):
	def handle_request(self):
		responder = HTTP_Responder(self.socket)
		path = self.path[1:]

		if not os.path.isfile(path):
			responder.reply_invalid_file()
			return

class HTTP_handler(object):
	def __init__(self, client_socket):
		self.socket = client_socket

	def read_and_parse_request(self, wait_time=MAX_WAIT_TIME):
		start_time = time.time()
		request = ''
		buffering = True
		while buffering:
			elapsed_time = time.time() - start_time
			if elapsed_time > wait_time:
				raise socket.timeout
			self.socket.settimeout(wait_time - elapsed_time)
			new_input = self.socket.recv(4096)

			if not new_input:
				buffering = False

			request += new_input

			# Check if we're done
			if '\r\n\r\n' in request:
				status_line, headers, body = self.parse_request(request)
				temp_headers = map(lambda x: x.split(':',1), headers)
				temp_headers = map(lambda y: (y[0].lower(), y[1]), temp_headers)
				header_dict = dict(temp_headers)
				if int(header_dict.get('content-length', 0)) == len(body):
					return status_line, temp_headers, body

		raise BadRequestException

	def parse_request(self, request):
		try:
			status_and_headers, body = request.split('\r\n\r\n', 1)
		except ValueError:
			raise BadRequestException
		status_and_headers = status_and_headers.split('\r\n')
		status_line, headers = status_and_headers[0], status_and_headers[1:]
		return status_line, headers, body

	def method_and_path_from_line(self, first_line):
		try:
			method, path, _ = first_line.split(' ', 2)
			path = self.parse_and_normalize_path(path)
			return method, path
		except ValueError:
			raise BadRequestException
		except:
			raise

	def parse_and_normalize_path(self, path):
		p = re.compile('^/([\w\-.~/]|%[0-9A-Fa-f]{2})*$')
		if not p.match(path):
			raise BadRequestException

		def percent_decode(m):
			s = m.group(1)
			return s[1:].decode('hex')

		decoded_path = re.sub('(%[0-9A-Fa-f]{2})', percent_decode, path)
		normalized_path = os.path.normpath(decoded_path)

		return normalized_path

	def route_request(self):
		""" Parses the first line, and hands off the connection to either
			the static file handler or the cgi handler. """

		# Get entire http request: status_line, headers, body
		try:
			status_line, headers, body = self.read_and_parse_request()
		except socket.timeout:
			HTTP_Responder(self.socket).reply_timed_out()
			return
		except BadRequestException:
			HTTP_Responder(self.socket).reply_invalid_request()
			return

		# Split status line into method and path
		try:
			method, path = self.method_and_path_from_line(status_line)
		except BadRequestException:
			HTTP_Responder(self.socket).reply_invalid_request()
			return

		# Route request to the CGI or static handler
		if path.startswith('/cgi-bin/'):
			handler = CGI_handler(method, path, headers, body, self.socket)
		else:
			handler = Static_handler(method, path, headers, body, self.socket)

		handler.handle_request()

class Handler_thread(threading.Thread):
	def run(self, client_socket):
		new_handler = HTTP_handler(client_socket)
		new_handler.route_request()<|endoftext|>