Repository: Delgan/loguru (python)

Issue:
Replace Travis CI with GitHub Actions
See https://github.com/tartley/colorama/pull/322

Pull request description:
Replace Travis CI with GitHub Actions
Added GitHub Actions file with `tox` runs and `codecov` reporting.
Switched `pytest-cov` to output XML for Codecov action (otherwise it failed).
Deleted Travis CI configuration.

Fixes #518

Patch:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d5e4ddf..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-language: python
-
-os: linux
-
-dist: xenial
-
-python:
-  - '3.5.2'
-  - '3.5'
-  - '3.6'
-  - '3.7'
-  - '3.8'
-  - '3.9'
-  - 'pypy3.5'
-
-install:
-  - pip install tox-travis codecov
-
-script: tox
-
-after_success: |
-  VERSION="python_${TRAVIS_PYTHON_VERSION//[^[:alnum:]]/}" &&
-  codecov --required --flags $VERSION ||
-  (sleep 5 && codecov --required --flags $VERSION) ||
-  (sleep 10 && codecov --required --flags $VERSION) ||
-  (sleep 20 && codecov --required --flags $VERSION) ||
-  (sleep 30 && codecov --required --flags $VERSION)
diff --git a/README.rst b/README.rst
index 4c643b1..07f03ef 100644
--- a/README.rst
+++ b/README.rst
@@ -11,7 +11,7 @@
         <a href="https://pypi.python.org/pypi/loguru"><img alt="Pypi version" src="https://img.shields.io/pypi/v/loguru.svg"></a>
         <a href="https://pypi.python.org/pypi/loguru"><img alt="Python versions" src="https://img.shields.io/badge/python-3.5%2B%20%7C%20PyPy-blue.svg"></a>
         <a href="https://loguru.readthedocs.io/en/stable/index.html"><img alt="Documentation" src="https://img.shields.io/readthedocs/loguru.svg"></a>
-        <a href="https://travis-ci.com/Delgan/loguru"><img alt="Build status" src="https://img.shields.io/travis/Delgan/loguru/master.svg"></a>
+        <a href="https://github.com/Delgan/loguru/actions/workflows/tests.yml?query=branch:master"><img alt="Build status" src="https://img.shields.io/github/workflow/status/Delgan/loguru/Tests/master"></a>
         <a href="https://codecov.io/gh/delgan/loguru/branch/master"><img alt="Coverage" src="https://img.shields.io/codecov/c/github/delgan/loguru/master.svg"></a>
         <a href="https://app.codacy.com/gh/Delgan/loguru/dashboard"><img alt="Code quality" src="https://img.shields.io/codacy/grade/be7337df3c0d40d1929eb7f79b1671a6.svg"></a>
         <a href="https://github.com/Delgan/loguru/blob/master/LICENSE"><img alt="License" src="https://img.shields.io/github/license/delgan/loguru.svg"></a>
diff --git a/loguru/_contextvars.py b/loguru/_contextvars.py
new file mode 100644
index 0000000..2e8bbb2
--- /dev/null
+++ b/loguru/_contextvars.py
@@ -0,0 +1,15 @@
+import sys
+
+
+def load_contextvar_class():
+    if sys.version_info >= (3, 7):
+        from contextvars import ContextVar
+    elif sys.version_info >= (3, 5, 3):
+        from aiocontextvars import ContextVar
+    else:
+        from contextvars import ContextVar
+
+    return ContextVar
+
+
+ContextVar = load_contextvar_class()
diff --git a/loguru/_ctime_functions.py b/loguru/_ctime_functions.py
index a86287c..df20c1a 100644
--- a/loguru/_ctime_functions.py
+++ b/loguru/_ctime_functions.py
@@ -1,49 +1,53 @@
 import os
 
-if os.name == "nt":
-    import win32_setctime
 
-    def get_ctime(filepath):
-        return os.stat(filepath).st_ctime
+def load_ctime_functions():
+    if os.name == "nt":
+        import win32_setctime
 
-    def set_ctime(filepath, timestamp):
-        if not win32_setctime.SUPPORTED:
-            return
+        def get_ctime(filepath):
+            return os.stat(filepath).st_ctime
 
-        try:
-            win32_setctime.setctime(filepath, timestamp)
-        except (OSError, ValueError):
-            pass
+        def set_ctime(filepath, timestamp):
+            if not win32_setctime.SUPPORTED:
+                return
+
+            try:
+                win32_setctime.setctime(filepath, timestamp)
+            except (OSError, ValueError):
+                pass
 
+    elif hasattr(os.stat_result, "st_birthtime"):
 
-elif hasattr(os.stat_result, "st_birthtime"):
+        def get_ctime(filepath):
+            return os.stat(filepath).st_birthtime
 
-    def get_ctime(filepath):
-        return os.stat(filepath).st_birthtime
+        def set_ctime(filepath, timestamp):
+            pass
 
-    def set_ctime(filepath, timestamp):
-        pass
+    elif hasattr(os, "getxattr") and hasattr(os, "setxattr"):
 
+        def get_ctime(filepath):
+            try:
+                return float(os.getxattr(filepath, b"user.loguru_crtime"))
+            except OSError:
+                return os.stat(filepath).st_mtime
 
-elif hasattr(os, "getxattr") and hasattr(os, "setxattr"):
+        def set_ctime(filepath, timestamp):
+            try:
+                os.setxattr(filepath, b"user.loguru_crtime", str(timestamp).encode("ascii"))
+            except OSError:
+                pass
 
-    def get_ctime(filepath):
-        try:
-            return float(os.getxattr(filepath, b"user.loguru_crtime"))
-        except OSError:
+    else:
+
+        def get_ctime(filepath):
             return os.stat(filepath).st_mtime
 
-    def set_ctime(filepath, timestamp):
-        try:
-            os.setxattr(filepath, b"user.loguru_crtime", str(timestamp).encode("ascii"))
-        except OSError:
+        def set_ctime(filepath, timestamp):
             pass
 
+    return get_ctime, set_ctime
 
-else:
-
-    def get_ctime(filepath):
-        return os.stat(filepath).st_mtime
 
-    def set_ctime(filepath, timestamp):
-        pass
+get_ctime, set_ctime = load_ctime_functions()
diff --git a/loguru/_get_frame.py b/loguru/_get_frame.py
index 589d9c7..ee6eaab 100644
--- a/loguru/_get_frame.py
+++ b/loguru/_get_frame.py
@@ -12,7 +12,12 @@ def get_frame_fallback(n):
         return frame
 
 
-if hasattr(sys, "_getframe"):
-    get_frame = sys._getframe
-else:
-    get_frame = get_frame_fallback
+def load_get_frame_function():
+    if hasattr(sys, "_getframe"):
+        get_frame = sys._getframe
+    else:
+        get_frame = get_frame_fallback
+    return get_frame
+
+
+get_frame = load_get_frame_function()
diff --git a/loguru/_locks_machinery.py b/loguru/_locks_machinery.py
index b1920ba..6f02110 100644
--- a/loguru/_locks_machinery.py
+++ b/loguru/_locks_machinery.py
@@ -10,7 +10,6 @@ if not hasattr(os, "register_at_fork"):
     def create_handler_lock():
         return threading.Lock()
 
-
 else:
     # While forking, we need to sanitize all locks to make sure the child process doesn't run into
     # a deadlock (if a lock already acquired is inherited) and to protect sink from corrupted state.
diff --git a/loguru/_logger.py b/loguru/_logger.py
index 4a1125f..6d27b72 100644
--- a/loguru/_logger.py
+++ b/loguru/_logger.py
@@ -90,6 +90,7 @@ from threading import current_thread
 from . import _colorama, _defaults, _filters
 from ._better_exceptions import ExceptionFormatter
 from ._colorizer import Colorizer
+from ._contextvars import ContextVar
 from ._datetime import aware_now
 from ._error_interceptor import ErrorInterceptor
 from ._file_sink import FileSink
@@ -104,13 +105,6 @@ if sys.version_info >= (3, 6):
 else:
     from pathlib import PurePath as PathLike
 
-if sys.version_info >= (3, 7):
-    from contextvars import ContextVar
-elif sys.version_info >= (3, 5, 3):
-    from aiocontextvars import ContextVar
-else:
-    from contextvars import ContextVar
-
 
 Level = namedtuple("Level", ["name", "no", "color", "icon"])
 
diff --git a/loguru/_simple_sinks.py b/loguru/_simple_sinks.py
index 6605fa1..0597a1d 100644
--- a/loguru/_simple_sinks.py
+++ b/loguru/_simple_sinks.py
@@ -8,7 +8,6 @@ if sys.version_info >= (3, 7):
     def get_loop(task):
         return task.get_loop()
 
-
 else:
 
     def get_loop(task):
diff --git a/setup.py b/setup.py
index 3f9e7e6..8eda560 100644
--- a/setup.py
+++ b/setup.py
@@ -56,18 +56,16 @@ setup(
     extras_require={
         "dev": [
             "black>=19.10b0 ; python_version>='3.6'",
-            "codecov>=2.0.15",
             "colorama>=0.3.4",
             "docutils==0.16",
             "flake8>=3.7.7",
             "isort>=5.1.1 ; python_version>='3.6'",
             "tox>=3.9.0",
-            "tox-travis>=0.12",
             "pytest>=4.6.2",
             "pytest-cov>=2.7.1",
-            "Sphinx>=4.1.1",
+            "Sphinx>=4.1.1 ; python_version>='3.6'",
             "sphinx-autobuild>=0.7.1 ; python_version>='3.6'",
-            "sphinx-rtd-theme>=0.4.3",
+            "sphinx-rtd-theme>=0.4.3 ; python_version>='3.6'",
         ]
     },
     python_requires=">=3.5",
diff --git a/tox.ini b/tox.ini
index e04e6f7..aeeceab 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,7 @@ setenv = PYTHONPATH = {toxinidir}
 extras = dev
 commands =
     flake8 --exit-zero loguru tests docs setup.py
-    pytest --cov loguru/
+    pytest --cov loguru/ --cov-report=xml
     coverage report -m
 
 [testenv:format]


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..f41ef83
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,45 @@
+name: Tests
+
+on:
+  push:
+  schedule:
+      - cron: 0 0 * * 0
+
+jobs:
+  tests:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os:
+          - ubuntu-latest
+        python-version:
+          - '3.5'
+          - '3.6'
+          - '3.7'
+          - '3.8'
+          - '3.9'
+          - 'pypy-3.8'
+        include:
+          - os: windows-latest
+            python-version: '3.9'
+          - os: macos-latest
+            python-version: '3.9'
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install tox
+      - name: Run tox
+        run: |
+          tox -e py
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v2
+        with:
+          flags: unittests
+          verbose: true
diff --git a/tests/conftest.py b/tests/conftest.py
index 70f4864..0d305b2 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -174,11 +174,11 @@ def make_logging_logger(name, handler, fmt="%(message)s", level="DEBUG"):
 
 @pytest.fixture
 def f_globals_name_absent(monkeypatch):
-    getframe_ = loguru._get_frame.get_frame
+    getframe_ = loguru._get_frame.load_get_frame_function()
 
     def patched_getframe(*args, **kwargs):
         frame = getframe_(*args, **kwargs)
-        monkeypatch.delitem(frame.f_globals, "__name__", raising=False)
+        frame.f_globals.pop("__name__", None)
         return frame
 
     monkeypatch.setattr(loguru._logger, "get_frame", patched_getframe)
diff --git a/tests/test_add_option_enqueue.py b/tests/test_add_option_enqueue.py
index f1d3679..0b43800 100644
--- a/tests/test_add_option_enqueue.py
+++ b/tests/test_add_option_enqueue.py
@@ -43,7 +43,7 @@ def test_enqueue():
     logger.add(sink, format="{message}", enqueue=True)
     logger.debug("Test")
     assert len(x) == 0
-    time.sleep(0.2)
+    logger.complete()
     assert len(x) == 1
     assert x[0] == "Test\n"
 
@@ -63,7 +63,7 @@ def test_enqueue_with_exception():
         logger.exception("Error")
 
     assert len(x) == 0
-    time.sleep(0.2)
+    logger.complete()
     assert len(x) == 1
     lines = x[0].splitlines()
 
diff --git a/tests/test_contextualize.py b/tests/test_contextualize.py
index 98d50fd..985f64f 100644
--- a/tests/test_contextualize.py
+++ b/tests/test_contextualize.py
@@ -1,10 +1,12 @@
 import asyncio
 import sys
 import threading
+from unittest.mock import MagicMock
 
 import pytest
 
 from loguru import logger
+from loguru._contextvars import load_contextvar_class
 
 
 def test_contextualize(writer):
@@ -207,3 +209,12 @@ def test_context_reset_despite_error(writer):
         logger.info("Error")
 
     assert writer.read() == "Division {'foobar': 456}\nError {}\n"
+
+
+# There is not CI runner available for Python 3.5.2. Consequently, we are just
+# verifying third-library is properly imported to reach 100% coverage.
+def test_contextvars_fallback_352(monkeypatch):
+    mock_module = MagicMock()
+    monkeypatch.setattr(sys, "version_info", (3, 5, 2))
+    monkeypatch.setitem(sys.modules, "contextvars", mock_module)
+    assert load_contextvar_class() == mock_module.ContextVar
diff --git a/tests/test_coroutine_sink.py b/tests/test_coroutine_sink.py
index f72b59e..626559f 100644
--- a/tests/test_coroutine_sink.py
+++ b/tests/test_coroutine_sink.py
@@ -1,6 +1,7 @@
 import asyncio
 import logging
 import multiprocessing
+import platform
 import re
 import sys
 import threading
@@ -689,6 +690,7 @@ class Writer:
         self.output += message
 
 
+@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy bug #3630")
 def test_complete_with_sub_processes(monkeypatch, capsys):
     ctx = multiprocessing.get_context("spawn")
     monkeypatch.setattr(loguru._handler, "multiprocessing", ctx)
diff --git a/tests/test_exceptions_formatting.py b/tests/test_exceptions_formatting.py
index cad2dba..3334d0e 100644
--- a/tests/test_exceptions_formatting.py
+++ b/tests/test_exceptions_formatting.py
@@ -30,7 +30,7 @@ def normalize(exception):
                 relpath = "".join(groups)
             else:
                 relpath = os.path.relpath(os.path.abspath(filepath), start_directory)
-            return 'File "%s"' % relpath
+            return 'File "%s"' % relpath.replace("\\", "/")
 
         exception = re.sub(
             r'File "([^"]+\.py[^"]*)"',
diff --git a/tests/test_filesink_rotation.py b/tests/test_filesink_rotation.py
index 205a272..bdf75a3 100644
--- a/tests/test_filesink_rotation.py
+++ b/tests/test_filesink_rotation.py
@@ -1,6 +1,5 @@
 import builtins
 import datetime
-import importlib
 import os
 import pathlib
 import re
@@ -13,6 +12,7 @@ import pytest
 
 import loguru
 from loguru import logger
+from loguru._ctime_functions import load_ctime_functions
 
 
 @pytest.fixture
@@ -24,9 +24,9 @@ def tmpdir_local(reset_logger):
 
 
 def reload_filesink_ctime_functions(monkeypatch):
-    ctime_functions = importlib.reload(loguru._ctime_functions)
-    monkeypatch.setattr(loguru._file_sink, "get_ctime", ctime_functions.get_ctime)
-    monkeypatch.setattr(loguru._file_sink, "set_ctime", ctime_functions.set_ctime)
+    get_ctime, set_ctime = load_ctime_functions()
+    monkeypatch.setattr(loguru._file_sink, "get_ctime", get_ctime)
+    monkeypatch.setattr(loguru._file_sink, "set_ctime", set_ctime)
 
 
 @pytest.fixture
diff --git a/tests/test_get_frame.py b/tests/test_get_frame.py
index ae7bfe4..9dc40af 100644
--- a/tests/test_get_frame.py
+++ b/tests/test_get_frame.py
@@ -1,7 +1,7 @@
-import importlib
 import sys
 
 import loguru
+from loguru._get_frame import load_get_frame_function
 
 
 def test_with_sys_getframe(monkeypatch):
@@ -9,14 +9,12 @@ def test_with_sys_getframe(monkeypatch):
         return
 
     monkeypatch.setattr(sys, "_getframe", patched())
-    get_frame_module = importlib.reload(loguru._get_frame)
-    assert get_frame_module.get_frame == patched()
+    assert load_get_frame_function() == patched()
 
 
 def test_without_sys_getframe(monkeypatch):
     monkeypatch.delattr(sys, "_getframe")
-    get_frame_module = importlib.reload(loguru._get_frame)
-    assert get_frame_module.get_frame == loguru._get_frame.get_frame_fallback
+    assert load_get_frame_function() == loguru._get_frame.get_frame_fallback
 
 
 def test_get_frame_fallback():
diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py
index 914688b..a021d85 100644
--- a/tests/test_multiprocessing.py
+++ b/tests/test_multiprocessing.py
@@ -1,6 +1,5 @@
 import asyncio
 import copy
-import multiprocessing
 import os
 import platform
 import sys
@@ -13,6 +12,24 @@ import loguru
 from loguru import logger
 
 
+@pytest.fixture
+def fork_context(monkeypatch):
+    import multiprocessing
+
+    context = multiprocessing.get_context("fork")
+    monkeypatch.setattr(loguru._handler, "multiprocessing", context)
+    yield context
+
+
+@pytest.fixture
+def spawn_context(monkeypatch):
+    import multiprocessing
+
+    context = multiprocessing.get_context("spawn")
+    monkeypatch.setattr(loguru._handler, "multiprocessing", context)
+    yield context
+
+
 def do_something(i):
     logger.info("#{}", i)
 
@@ -87,15 +104,13 @@ class Writer:
         return self._output
 
 
-def test_apply_spawn(monkeypatch):
-    ctx = multiprocessing.get_context("spawn")
-    monkeypatch.setattr(loguru._handler, "multiprocessing", ctx)
-
+@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy bug #3630")
+def test_apply_spawn(spawn_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    with ctx.Pool(1, set_logger, [logger]) as pool:
+    with spawn_context.Pool(1, set_logger, [logger]) as pool:
         for i in range(3):
             pool.apply(do_something, (i,))
         pool.close()
@@ -108,12 +123,12 @@ def test_apply_spawn(monkeypatch):
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_apply_fork():
+def test_apply_fork(fork_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    with multiprocessing.Pool(1, set_logger, [logger]) as pool:
+    with fork_context.Pool(1, set_logger, [logger]) as pool:
         for i in range(3):
             pool.apply(do_something, (i,))
         pool.close()
@@ -126,12 +141,12 @@ def test_apply_fork():
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_apply_inheritance():
+def test_apply_inheritance(fork_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    with multiprocessing.Pool(1) as pool:
+    with fork_context.Pool(1) as pool:
         for i in range(3):
             pool.apply(do_something, (i,))
         pool.close()
@@ -143,15 +158,13 @@ def test_apply_inheritance():
     assert writer.read() == "#0\n#1\n#2\nDone!\n"
 
 
-def test_apply_async_spawn(monkeypatch):
-    ctx = multiprocessing.get_context("spawn")
-    monkeypatch.setattr(loguru._handler, "multiprocessing", ctx)
-
+@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy bug #3630")
+def test_apply_async_spawn(spawn_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    with ctx.Pool(1, set_logger, [logger]) as pool:
+    with spawn_context.Pool(1, set_logger, [logger]) as pool:
         for i in range(3):
             result = pool.apply_async(do_something, (i,))
             result.get()
@@ -165,12 +178,12 @@ def test_apply_async_spawn(monkeypatch):
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_apply_async_fork():
+def test_apply_async_fork(fork_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    with multiprocessing.Pool(1, set_logger, [logger]) as pool:
+    with fork_context.Pool(1, set_logger, [logger]) as pool:
         for i in range(3):
             result = pool.apply_async(do_something, (i,))
             result.get()
@@ -184,12 +197,12 @@ def test_apply_async_fork():
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_apply_async_inheritance():
+def test_apply_async_inheritance(fork_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    with multiprocessing.Pool(1) as pool:
+    with fork_context.Pool(1) as pool:
         for i in range(3):
             result = pool.apply_async(do_something, (i,))
             result.get()
@@ -202,15 +215,13 @@ def test_apply_async_inheritance():
     assert writer.read() == "#0\n#1\n#2\nDone!\n"
 
 
-def test_process_spawn(monkeypatch):
-    ctx = multiprocessing.get_context("spawn")
-    monkeypatch.setattr(loguru._handler, "multiprocessing", ctx)
-
+@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy bug #3630")
+def test_process_spawn(spawn_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = ctx.Process(target=subworker, args=(logger,))
+    process = spawn_context.Process(target=subworker, args=(logger,))
     process.start()
     process.join()
 
@@ -223,12 +234,12 @@ def test_process_spawn(monkeypatch):
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_process_fork():
+def test_process_fork(fork_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker, args=(logger,))
+    process = fork_context.Process(target=subworker, args=(logger,))
     process.start()
     process.join()
 
@@ -241,12 +252,12 @@ def test_process_fork():
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_process_inheritance():
+def test_process_inheritance(fork_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker_inheritance)
+    process = fork_context.Process(target=subworker_inheritance)
     process.start()
     process.join()
 
@@ -258,15 +269,13 @@ def test_process_inheritance():
     assert writer.read() == "Child\nMain\n"
 
 
-def test_remove_in_child_process_spawn(monkeypatch):
-    ctx = multiprocessing.get_context("spawn")
-    monkeypatch.setattr(loguru._handler, "multiprocessing", ctx)
-
+@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy bug #3630")
+def test_remove_in_child_process_spawn(spawn_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = ctx.Process(target=subworker_remove, args=(logger,))
+    process = spawn_context.Process(target=subworker_remove, args=(logger,))
     process.start()
     process.join()
 
@@ -279,12 +288,12 @@ def test_remove_in_child_process_spawn(monkeypatch):
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_remove_in_child_process_fork():
+def test_remove_in_child_process_fork(fork_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker_remove, args=(logger,))
+    process = fork_context.Process(target=subworker_remove, args=(logger,))
     process.start()
     process.join()
 
@@ -297,12 +306,12 @@ def test_remove_in_child_process_fork():
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_remove_in_child_process_inheritance():
+def test_remove_in_child_process_inheritance(fork_context):
     writer = Writer()
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker_remove_inheritance)
+    process = fork_context.Process(target=subworker_remove_inheritance)
     process.start()
     process.join()
 
@@ -314,19 +323,17 @@ def test_remove_in_child_process_inheritance():
     assert writer.read() == "Child\nMain\n"
 
 
-def test_remove_in_main_process_spawn(monkeypatch):
+@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy bug #3630")
+def test_remove_in_main_process_spawn(spawn_context):
     # Actually, this test may fail if sleep time in main process is too small (and no barrier used)
     # In such situation, it seems the child process has not enough time to initialize itself
     # It may fail with an "EOFError" during unpickling of the (garbage collected / closed) Queue
-    ctx = multiprocessing.get_context("spawn")
-    monkeypatch.setattr(loguru._handler, "multiprocessing", ctx)
-
     writer = Writer()
-    barrier = ctx.Barrier(2)
+    barrier = spawn_context.Barrier(2)
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = ctx.Process(target=subworker_barrier, args=(logger, barrier))
+    process = spawn_context.Process(target=subworker_barrier, args=(logger, barrier))
     process.start()
     barrier.wait()
     logger.info("Main")
@@ -339,13 +346,13 @@ def test_remove_in_main_process_spawn(monkeypatch):
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_remove_in_main_process_fork():
+def test_remove_in_main_process_fork(fork_context):
     writer = Writer()
-    barrier = multiprocessing.Barrier(2)
+    barrier = fork_context.Barrier(2)
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker_barrier, args=(logger, barrier))
+    process = fork_context.Process(target=subworker_barrier, args=(logger, barrier))
     process.start()
     barrier.wait()
     logger.info("Main")
@@ -358,13 +365,13 @@ def test_remove_in_main_process_fork():
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_remove_in_main_process_inheritance():
+def test_remove_in_main_process_inheritance(fork_context):
     writer = Writer()
-    barrier = multiprocessing.Barrier(2)
+    barrier = fork_context.Barrier(2)
 
     logger.add(writer, format="{message}", enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker_barrier_inheritance, args=(barrier,))
+    process = fork_context.Process(target=subworker_barrier_inheritance, args=(barrier,))
     process.start()
     barrier.wait()
     logger.info("Main")
@@ -376,11 +383,9 @@ def test_remove_in_main_process_inheritance():
     assert writer.read() == "Child\nMain\n"
 
 
+@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy bug #3630")
 @pytest.mark.parametrize("loop_is_none", [True, False])
-def test_await_complete_spawn(capsys, monkeypatch, loop_is_none):
-    ctx = multiprocessing.get_context("spawn")
-    monkeypatch.setattr(loguru._handler, "multiprocessing", ctx)
-
+def test_await_complete_spawn(capsys, spawn_context, loop_is_none):
     async def writer(msg):
         print(msg, end="")
 
@@ -390,7 +395,7 @@ def test_await_complete_spawn(capsys, monkeypatch, loop_is_none):
 
     logger.add(writer, format="{message}", loop=loop, enqueue=True, catch=False)
 
-    process = ctx.Process(target=subworker_complete, args=(logger,))
+    process = spawn_context.Process(target=subworker_complete, args=(logger,))
     process.start()
     process.join()
 
@@ -408,7 +413,7 @@ def test_await_complete_spawn(capsys, monkeypatch, loop_is_none):
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
 @pytest.mark.parametrize("loop_is_none", [True, False])
-def test_await_complete_fork(capsys, loop_is_none):
+def test_await_complete_fork(capsys, loop_is_none, fork_context):
     async def writer(msg):
         print(msg, end="")
 
@@ -418,7 +423,7 @@ def test_await_complete_fork(capsys, loop_is_none):
 
     logger.add(writer, format="{message}", loop=loop, enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker_complete, args=(logger,))
+    process = fork_context.Process(target=subworker_complete, args=(logger,))
     process.start()
     process.join()
 
@@ -436,7 +441,7 @@ def test_await_complete_fork(capsys, loop_is_none):
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
 @pytest.mark.parametrize("loop_is_none", [True, False])
-def test_await_complete_inheritance(capsys, loop_is_none):
+def test_await_complete_inheritance(capsys, loop_is_none, fork_context):
     async def writer(msg):
         print(msg, end="")
 
@@ -446,7 +451,7 @@ def test_await_complete_inheritance(capsys, loop_is_none):
 
     logger.add(writer, format="{message}", loop=loop, enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker_complete_inheritance)
+    process = fork_context.Process(target=subworker_complete_inheritance)
     process.start()
     process.join()
 
@@ -462,10 +467,8 @@ def test_await_complete_inheritance(capsys, loop_is_none):
     assert err == ""
 
 
-def test_not_picklable_sinks_spawn(monkeypatch, tmpdir, capsys):
-    ctx = multiprocessing.get_context("spawn")
-    monkeypatch.setattr(loguru._handler, "multiprocessing", ctx)
-
+@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy bug #3630")
+def test_not_picklable_sinks_spawn(spawn_context, tmpdir, capsys):
     filepath = tmpdir.join("test.log")
     stream = sys.stderr
     output = []
@@ -474,7 +477,7 @@ def test_not_picklable_sinks_spawn(monkeypatch, tmpdir, capsys):
     logger.add(stream, format="{message}", enqueue=True)
     logger.add(lambda m: output.append(m), format="{message}", enqueue=True)
 
-    process = ctx.Process(target=subworker, args=[logger])
+    process = spawn_context.Process(target=subworker, args=[logger])
     process.start()
     process.join()
 
@@ -492,7 +495,7 @@ def test_not_picklable_sinks_spawn(monkeypatch, tmpdir, capsys):
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_not_picklable_sinks_fork(capsys, tmpdir):
+def test_not_picklable_sinks_fork(capsys, tmpdir, fork_context):
     filepath = tmpdir.join("test.log")
     stream = sys.stderr
     output = []
@@ -501,7 +504,7 @@ def test_not_picklable_sinks_fork(capsys, tmpdir):
     logger.add(stream, format="{message}", enqueue=True, catch=False)
     logger.add(lambda m: output.append(m), format="{message}", enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker, args=[logger])
+    process = fork_context.Process(target=subworker, args=[logger])
     process.start()
     process.join()
 
@@ -519,7 +522,7 @@ def test_not_picklable_sinks_fork(capsys, tmpdir):
 
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
-def test_not_picklable_sinks_inheritance(capsys, tmpdir):
+def test_not_picklable_sinks_inheritance(capsys, tmpdir, fork_context):
     filepath = tmpdir.join("test.log")
     stream = sys.stderr
     output = []
@@ -528,7 +531,7 @@ def test_not_picklable_sinks_inheritance(capsys, tmpdir):
     logger.add(stream, format="{message}", enqueue=True, catch=False)
     logger.add(lambda m: output.append(m), format="{message}", enqueue=True, catch=False)
 
-    process = multiprocessing.Process(target=subworker_inheritance)
+    process = fork_context.Process(target=subworker_inheritance)
     process.start()
     process.join()
 
@@ -549,7 +552,7 @@ def test_not_picklable_sinks_inheritance(capsys, tmpdir):
 @pytest.mark.skipif(sys.version_info < (3, 7), reason="No 'os.register_at_fork()' function")
 @pytest.mark.parametrize("enqueue", [True, False])
 @pytest.mark.parametrize("deepcopied", [True, False])
-def test_no_deadlock_if_internal_lock_in_use(tmpdir, enqueue, deepcopied):
+def test_no_deadlock_if_internal_lock_in_use(tmpdir, enqueue, deepcopied, fork_context):
     if deepcopied:
         logger_ = copy.deepcopy(logger)
     else:
@@ -574,7 +577,7 @@ def test_no_deadlock_if_internal_lock_in_use(tmpdir, enqueue, deepcopied):
     thread = threading.Thread(target=main)
     thread.start()
 
-    process = multiprocessing.Process(target=worker)
+    process = fork_context.Process(target=worker)
     process.start()
 
     thread.join()
@@ -590,14 +593,14 @@ def test_no_deadlock_if_internal_lock_in_use(tmpdir, enqueue, deepcopied):
 @pytest.mark.skipif(sys.version_info < (3, 7), reason="No 'os.register_at_fork()' function")
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
 @pytest.mark.parametrize("enqueue", [True, False])
-def test_no_deadlock_if_external_lock_in_use(enqueue, capsys):
+def test_no_deadlock_if_external_lock_in_use(enqueue, capsys, fork_context):
     # Can't reproduce the bug on pytest (even if stderr is not wrapped), but let it anyway
     logger.add(sys.stderr, enqueue=enqueue, catch=True, format="{message}")
     num = 100
 
     for i in range(num):
         logger.info("This is a message: {}", i)
-        process = multiprocessing.Process(target=lambda: None)
+        process = fork_context.Process(target=lambda: None)
         process.start()
         process.join(1)
         assert process.exitcode == 0
@@ -611,11 +614,11 @@ def test_no_deadlock_if_external_lock_in_use(enqueue, capsys):
 
 @pytest.mark.skipif(os.name == "nt", reason="Windows does not support forking")
 @pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="PyPy is too slow")
-def test_complete_from_multiple_child_processes(capsys):
+def test_complete_from_multiple_child_processes(capsys, fork_context):
     logger.add(lambda _: None, enqueue=True, catch=False)
     num = 100
 
-    barrier = multiprocessing.Barrier(num)
+    barrier = fork_context.Barrier(num)
 
     def worker(barrier):
         barrier.wait()
@@ -624,7 +627,7 @@ def test_complete_from_multiple_child_processes(capsys):
     processes = []
 
     for _ in range(num):
-        process = multiprocessing.Process(target=worker, args=(barrier,))
+        process = fork_context.Process(target=worker, args=(barrier,))
         process.start()
         processes.append(process)