diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a68a8c..e9544ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.6', '3.7', '3.8', '3.9'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - name: Checkout uses: actions/checkout@v2 @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.6', '3.6', '3.7', '3.8', '3.9', '3.8-dev', '3.9-dev'] + python: ['pypy-3.7', '3.7', 'pypy-3.8', '3.8', 'pypy-3.9', '3.9', '3.10', '3.11'] check_formatting: ['0'] check_docs: ['0'] extra_name: [''] @@ -74,7 +74,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.6', '3.7', '3.8', '3.9'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/.style.yapf b/.style.yapf deleted file mode 100644 index 2c71c9d..0000000 --- a/.style.yapf +++ /dev/null @@ -1,180 +0,0 @@ -[style] -# Align closing bracket with visual indentation. -align_closing_bracket_with_visual_indent=True - -# Allow dictionary keys to exist on multiple lines. For example: -# -# x = { -# ('this is the first element of a tuple', -# 'this is the second element of a tuple'): -# value, -# } -allow_multiline_dictionary_keys=False - -# Allow lambdas to be formatted on more than one line. -allow_multiline_lambdas=False - -# Insert a blank line before a class-level docstring. -blank_line_before_class_docstring=False - -# Insert a blank line before a 'def' or 'class' immediately nested -# within another 'def' or 'class'. For example: -# -# class Foo: -# # <------ this blank line -# def method(): -# ... -blank_line_before_nested_class_or_def=False - -# Do not split consecutive brackets. Only relevant when -# dedent_closing_brackets is set. For example: -# -# call_func_that_takes_a_dict( -# { -# 'key1': 'value1', -# 'key2': 'value2', -# } -# ) -# -# would reformat to: -# -# call_func_that_takes_a_dict({ -# 'key1': 'value1', -# 'key2': 'value2', -# }) -coalesce_brackets=False - -# The column limit. -column_limit=79 - -# Indent width used for line continuations. -continuation_indent_width=4 - -# Put closing brackets on a separate line, dedented, if the bracketed -# expression can't fit in a single line. Applies to all kinds of brackets, -# including function definitions and calls. For example: -# -# config = { -# 'key1': 'value1', -# 'key2': 'value2', -# } # <--- this bracket is dedented and on a separate line -# -# time_series = self.remote_client.query_entity_counters( -# entity='dev3246.region1', -# key='dns.query_latency_tcp', -# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), -# start_ts=now()-timedelta(days=3), -# end_ts=now(), -# ) # <--- this bracket is dedented and on a separate line -dedent_closing_brackets=True - -# Place each dictionary entry onto its own line. -each_dict_entry_on_separate_line=True - -# The regex for an i18n comment. The presence of this comment stops -# reformatting of that line, because the comments are required to be -# next to the string they translate. -i18n_comment= - -# The i18n function call names. The presence of this function stops -# reformattting on that line, because the string it has cannot be moved -# away from the i18n comment. -i18n_function_call= - -# Indent the dictionary value if it cannot fit on the same line as the -# dictionary key. For example: -# -# config = { -# 'key1': -# 'value1', -# 'key2': value1 + -# value2, -# } -indent_dictionary_value=True - -# The number of columns to use for indentation. -indent_width=4 - -# Join short lines into one line. E.g., single line 'if' statements. -join_multiple_lines=False - -# Use spaces around default or named assigns. -spaces_around_default_or_named_assign=False - -# Use spaces around the power operator. -spaces_around_power_operator=False - -# The number of spaces required before a trailing comment. -spaces_before_comment=2 - -# Insert a space between the ending comma and closing bracket of a list, -# etc. -space_between_ending_comma_and_closing_bracket=False - -# Split before arguments if the argument list is terminated by a -# comma. -split_arguments_when_comma_terminated=True - -# Set to True to prefer splitting before '&', '|' or '^' rather than -# after. -split_before_bitwise_operator=True - -# Split before a dictionary or set generator (comp_for). For example, note -# the split before the 'for': -# -# foo = { -# variable: 'Hello world, have a nice day!' -# for variable in bar if variable != 42 -# } -split_before_dict_set_generator=True - -# If an argument / parameter list is going to be split, then split before -# the first argument. -split_before_first_argument=True - -# Set to True to prefer splitting before 'and' or 'or' rather than -# after. -split_before_logical_operator=True - -# Split named assignments onto individual lines. -split_before_named_assigns=True - -# The penalty for splitting right after the opening bracket. -split_penalty_after_opening_bracket=30 - -# The penalty for splitting the line after a unary operator. -split_penalty_after_unary_operator=10000 - -# The penalty for splitting right before an if expression. -split_penalty_before_if_expr=0 - -# The penalty of splitting the line around the '&', '|', and '^' -# operators. -split_penalty_bitwise_operator=300 - -# The penalty for characters over the column limit. -split_penalty_excess_character=4500 - -# The penalty incurred by adding a line split to the unwrapped line. The -# more line splits added the higher the penalty. -split_penalty_for_added_line_split=30 - -# The penalty of splitting a list of "import as" names. For example: -# -# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, -# long_argument_2, -# long_argument_3) -# -# would reformat to something like: -# -# from a_very_long_or_indented_module_name_yada_yad import ( -# long_argument_1, long_argument_2, long_argument_3) -split_penalty_import_names=0 - -# The penalty of splitting the line around the 'and' and 'or' -# operators. -split_penalty_logical_operator=0 - -# Use the Tab character for indentation. -use_tabs=False - diff --git a/CHEATSHEET.rst b/CHEATSHEET.rst index 49a8b8d..aeacf0a 100644 --- a/CHEATSHEET.rst +++ b/CHEATSHEET.rst @@ -9,15 +9,7 @@ To run tests * Actually run the tests: ``pytest pytest_trio`` - -To run yapf ------------ - -* Show what changes yapf wants to make: ``yapf -rpd setup.py - pytest_trio`` - -* Apply all changes directly to the source tree: ``yapf -rpi setup.py - pytest_trio`` +* Format the code with ``black .`` To make a release diff --git a/MANIFEST.in b/MANIFEST.in index 4edd4b1..afc90e2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include README.rst CHEATSHEET.rst LICENSE* CODE_OF_CONDUCT* CONTRIBUTING* -include .coveragerc .style.yapf +include .coveragerc include test-requirements.txt recursive-include docs * prune docs/build diff --git a/ci.sh b/ci.sh index ddcd21f..aae8b3c 100755 --- a/ci.sh +++ b/ci.sh @@ -6,9 +6,6 @@ set -ex -o pipefail uname -a env | sort -# See https://github.com/python-trio/trio/issues/334 -YAPF_VERSION=0.22.0 - # Curl's built-in retry system is not very robust; it gives up on lots of # network errors that we want to retry on. Wget might work better, but it's # not installed on azure pipelines's windows boxes. So... let's try some good @@ -33,16 +30,16 @@ python setup.py sdist --formats=zip python -m pip install dist/*.zip if [ "$CHECK_FORMATTING" = "1" ]; then - pip install yapf==${YAPF_VERSION} - if ! yapf -rpd setup.py pytest_trio; then + pip install black + if ! black --check . ; then cat <= 1.6.1 sphinx_rtd_theme sphinxcontrib-trio @@ -10,8 +9,6 @@ attrs >= 17.4.0 towncrier != 19.9.0,!= 21.3.0 # pytest-trio's own dependencies -trio >= 0.15.0 -async_generator >= 1.9 -outcome -# For node.get_closest_marker -pytest >= 3.6 +trio >= 0.22.0 +outcome >= 1.1.0 +pytest >= 7.2.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index b401bbf..ae0c01d 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile docs-requirements.in @@ -7,102 +7,110 @@ alabaster==0.7.12 # via sphinx async-generator==1.10 - # via - # -r docs-requirements.in - # trio -attrs==20.3.0 + # via trio +attrs==22.1.0 # via # -r docs-requirements.in # outcome # pytest # trio -babel==2.9.0 +babel==2.10.3 # via sphinx -certifi==2020.12.5 +certifi==2022.9.24 # via requests -chardet==4.0.0 +charset-normalizer==2.1.1 # via requests -click==7.1.2 +click==8.1.3 + # via + # click-default-group + # towncrier +click-default-group==1.2.2 # via towncrier -docutils==0.16 +docutils==0.17.1 # via # sphinx # sphinx-rtd-theme -idna==2.10 +exceptiongroup==1.0.0 + # via + # pytest + # trio +idna==3.4 # via # requests # trio -imagesize==1.2.0 +imagesize==1.4.1 + # via sphinx +importlib-metadata==5.0.0 # via sphinx -incremental==21.3.0 +incremental==22.10.0 # via towncrier iniconfig==1.1.1 # via pytest -jinja2==2.11.3 +jinja2==3.1.2 # via # sphinx # towncrier -markupsafe==1.1.1 +markupsafe==2.1.1 # via jinja2 -outcome==1.1.0 +outcome==1.2.0 # via # -r docs-requirements.in # trio -packaging==20.9 +packaging==21.3 # via # pytest # sphinx -pluggy==0.13.1 +pluggy==1.0.0 # via pytest -py==1.10.0 - # via pytest -pygments==2.8.1 +pygments==2.13.0 # via sphinx -pyparsing==2.4.7 +pyparsing==3.0.9 # via packaging -pytest==6.2.3 +pytest==7.2.0 # via -r docs-requirements.in -pytz==2021.1 +pytz==2022.5 # via babel -requests==2.25.1 +requests==2.28.1 # via sphinx -sniffio==1.2.0 +sniffio==1.3.0 # via trio -snowballstemmer==2.1.0 +snowballstemmer==2.2.0 # via sphinx -sortedcontainers==2.3.0 +sortedcontainers==2.4.0 # via trio -sphinx-rtd-theme==0.5.2 - # via -r docs-requirements.in -sphinx==3.5.4 +sphinx==5.3.0 # via # -r docs-requirements.in # sphinx-rtd-theme # sphinxcontrib-trio +sphinx-rtd-theme==1.0.0 + # via -r docs-requirements.in sphinxcontrib-applehelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-htmlhelp==2.0.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx -sphinxcontrib-serializinghtml==1.1.4 +sphinxcontrib-serializinghtml==1.1.5 # via sphinx sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in -toml==0.10.2 +tomli==2.0.1 # via # pytest # towncrier -towncrier==19.2.0 +towncrier==22.8.0 # via -r docs-requirements.in -trio==0.18.0 +trio==0.22.0 # via -r docs-requirements.in -urllib3==1.26.4 +urllib3==1.26.12 # via requests +zipp==3.10.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/docs/source/conf.py b/docs/source/conf.py index 70282d8..7a71ac8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,15 +19,18 @@ # import os import sys + # So autodoc can import our package -sys.path.insert(0, os.path.abspath('../..')) +sys.path.insert(0, os.path.abspath("../..")) # https://docs.readthedocs.io/en/stable/builds.html#build-environment if "READTHEDOCS" in os.environ: import glob + if glob.glob("../../newsfragments/*.*.rst"): print("-- Found newsfragments; running towncrier --", flush=True) import subprocess + subprocess.run( ["towncrier", "--yes", "--date", "not released yet"], cwd="../..", @@ -50,6 +53,7 @@ def setup(app): app.add_css_file("hackrtd.css") + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -60,36 +64,36 @@ def setup(app): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.coverage', - 'sphinx.ext.napoleon', - 'sphinxcontrib_trio', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.napoleon", + "sphinxcontrib_trio", ] intersphinx_mapping = { - "python": ('https://docs.python.org/3', None), - "trio": ('https://trio.readthedocs.io/en/stable', None), + "python": ("https://docs.python.org/3", None), + "trio": ("https://trio.readthedocs.io/en/stable", None), } autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +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' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'pytest-trio' -copyright = 'The pytest-trio authors' -author = 'The pytest-trio authors' +project = "pytest-trio" +copyright = "The pytest-trio authors" +author = "The pytest-trio 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 @@ -97,6 +101,7 @@ def setup(app): # # The short X.Y version. import pytest_trio + version = pytest_trio.__version__ # The full version, including alpha/beta/rc tags. release = version @@ -109,7 +114,7 @@ def setup(app): # # 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 +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -117,9 +122,9 @@ def setup(app): exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'default' +pygments_style = "default" -highlight_language = 'python3' +highlight_language = "python3" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -133,14 +138,13 @@ def setup(app): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -# -#html_theme = 'alabaster' # We have to set this ourselves, not only because it's useful for local # testing, but also because if we don't then RTD will throw away our # html_theme_options. import sphinx_rtd_theme -html_theme = 'sphinx_rtd_theme' + +html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme @@ -155,19 +159,19 @@ def setup(app): # versions/settings... "navigation_depth": 4, "logo_only": False, - 'prev_next_buttons_location': 'both' + "prev_next_buttons_location": "both", } # 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 = ['_static'] +html_static_path = ["_static"] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'pytest-triodoc' +htmlhelp_basename = "pytest-triodoc" # -- Options for LaTeX output --------------------------------------------- @@ -176,15 +180,12 @@ def setup(app): # 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', @@ -194,8 +195,7 @@ def setup(app): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'pytest-trio.tex', 'pytest-trio Documentation', - author, 'manual'), + (master_doc, "pytest-trio.tex", "pytest-trio Documentation", author, "manual"), ] @@ -204,8 +204,7 @@ def setup(app): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'pytest-trio', 'pytest-trio Documentation', - [author], 1) + (master_doc, "pytest-trio", "pytest-trio Documentation", [author], 1), ] @@ -215,7 +214,13 @@ def setup(app): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'pytest-trio', 'pytest-trio Documentation', - author, 'pytest-trio', 'pytest plugin for Trio', - 'Miscellaneous'), + ( + master_doc, + "pytest-trio", + "pytest-trio Documentation", + author, + "pytest-trio", + "pytest plugin for Trio", + "Miscellaneous", + ), ] diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 0ef3164..fe23cd1 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -93,17 +93,6 @@ And we're done! Let's try running pytest again: ============================== FAILURES ============================== __________________________ test_should_fail __________________________ - value = - - async def yield_(value=None): - > return await _yield_(value) - - venv/lib/python3.8/site-packages/async_generator/_impl.py:106: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - venv/lib/python3.8/site-packages/async_generator/_impl.py:99: in _yield_ - return (yield _wrap(value)) - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - async def test_should_fail(): > assert False E assert False @@ -179,13 +168,6 @@ regular pytest fixture:: # Teardown code, executed after the test is done await connection.execute("ROLLBACK") -If you need to support Python 3.5, which doesn't allow ``yield`` -inside an ``async def`` function, then you can define async fixtures -using the `async_generator -`__ -library – just make sure to put the ``@pytest.fixture`` *above* the -``@async_generator``. - .. _server-fixture-example: @@ -232,7 +214,7 @@ Here's a first attempt:: This will mostly work, but it has a few problems. The most obvious one is that when we run it, even if everything works perfectly, it will -hang at the end of the test – we never shut down the server, so the +hang at the end of the test - we never shut down the server, so the nursery block will wait forever for it to exit. To avoid this, we should cancel the nursery at the end of the test: @@ -292,9 +274,9 @@ you afterwards: Next problem: we have a race condition. We spawn a background task to call ``serve_tcp``, and then immediately try to connect to that server. Sometimes this will work fine. But it takes a little while for -the server to start up and be ready to accept connections – so other +the server to start up and be ready to accept connections - so other times, randomly, our connection attempt will happen too quickly, and -error out. After all – ``nursery.start_soon`` only promises that the +error out. After all - ``nursery.start_soon`` only promises that the task will be started *soon*, not that it has actually happened. So this test will be flaky, and flaky tests are the worst. @@ -378,7 +360,7 @@ Putting it all together: await echo_client.send_all(test_byte) assert await echo_client.receive_some(1) == test_byte -Now, this works – but there's still a lot of boilerplate. Remember, we +Now, this works - but there's still a lot of boilerplate. Remember, we need to write lots of tests for this server, and we don't want to have to copy-paste all that stuff into every test. Let's factor out the setup into a fixture:: diff --git a/newsfragments/129.misc.rst b/newsfragments/129.misc.rst new file mode 100644 index 0000000..13b403f --- /dev/null +++ b/newsfragments/129.misc.rst @@ -0,0 +1,2 @@ +Dropped support for end-of-life Python 3.6, and the ``async_generator`` library +necessary to support it, and started testing on Python 3.10 and 3.11. diff --git a/pytest.ini b/pytest.ini index e7e9beb..92c555f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = -W error -ra -v --pyargs pytest_trio --verbose --cov +addopts = -ra -v --pyargs pytest_trio --verbose --cov diff --git a/pytest_trio/_tests/helpers.py b/pytest_trio/_tests/helpers.py index 3715fa5..e54506b 100644 --- a/pytest_trio/_tests/helpers.py +++ b/pytest_trio/_tests/helpers.py @@ -6,9 +6,7 @@ def enable_trio_mode_via_pytest_ini(testdir): def enable_trio_mode_trio_run_via_pytest_ini(testdir): - testdir.makefile( - ".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = trio\n" - ) + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = trio\n") def enable_trio_mode_via_conftest_py(testdir): @@ -16,9 +14,10 @@ def enable_trio_mode_via_conftest_py(testdir): enable_trio_mode = pytest.mark.parametrize( - "enable_trio_mode", [ + "enable_trio_mode", + [ enable_trio_mode_via_pytest_ini, enable_trio_mode_trio_run_via_pytest_ini, enable_trio_mode_via_conftest_py, - ] + ], ) diff --git a/pytest_trio/_tests/test_async_fixture.py b/pytest_trio/_tests/test_async_fixture.py index 9cbd0b7..88c4aa9 100644 --- a/pytest_trio/_tests/test_async_fixture.py +++ b/pytest_trio/_tests/test_async_fixture.py @@ -120,7 +120,7 @@ async def test_simple(sync_fix): # they are actually run just before the test, providing no way to make the # difference between an exception coming from the real test or from an # async fixture... -@pytest.mark.xfail(reason='Not implemented yet') +@pytest.mark.xfail(reason="Not implemented yet") def test_raise_in_async_fixture_cause_pytest_error(testdir): testdir.makepyfile( diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 71dd362..58d1a55 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -1,40 +1,17 @@ -import sys -import pytest -import re - - -@pytest.fixture(params=['Python>=36', 'async_generator']) -def async_yield_implementation(request): - if request.param == 'Python>=36': - - def patch_code(code): - # Convert code to use Python>=3.6 builtin async generator - code = re.sub(r'(?m)^\s*@async_generator\n', r'', code) - code = re.sub(r'await yield_', r'yield', code) - return code - - return patch_code - else: - return lambda x: x - - -def test_single_async_yield_fixture(testdir, async_yield_implementation): +def test_single_async_yield_fixture(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ events = [] @pytest.fixture - @async_generator async def fix1(): events.append('fix1 setup') await trio.sleep(0) - await yield_('fix1') + yield 'fix1' await trio.sleep(0) events.append('fix1 teardown') @@ -53,7 +30,6 @@ def test_after(): 'fix1 teardown', ] """ - ) ) result = testdir.runpytest() @@ -61,35 +37,31 @@ def test_after(): result.assert_outcomes(passed=3) -def test_nested_async_yield_fixture(testdir, async_yield_implementation): +def test_nested_async_yield_fixture(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ events = [] @pytest.fixture - @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - await yield_('fix2') + yield 'fix2' await trio.sleep(0) events.append('fix2 teardown') @pytest.fixture - @async_generator async def fix1(fix2): events.append('fix1 setup') await trio.sleep(0) - await yield_('fix1') + yield 'fix1' await trio.sleep(0) events.append('fix1 teardown') @@ -113,7 +85,6 @@ def test_after(): 'fix2 teardown', ] """ - ) ) result = testdir.runpytest() @@ -121,26 +92,21 @@ def test_after(): result.assert_outcomes(passed=3) -def test_async_yield_fixture_within_sync_fixture( - testdir, async_yield_implementation -): +def test_async_yield_fixture_within_sync_fixture(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ events = [] @pytest.fixture - @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - await yield_('fix2') + yield 'fix2' await trio.sleep(0) events.append('fix2 teardown') @@ -165,7 +131,6 @@ def test_after(): 'fix2 teardown', ] """ - ) ) result = testdir.runpytest() @@ -173,26 +138,21 @@ def test_after(): result.assert_outcomes(passed=3) -def test_async_yield_fixture_within_sync_yield_fixture( - testdir, async_yield_implementation -): +def test_async_yield_fixture_within_sync_yield_fixture(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ events = [] @pytest.fixture - @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - await yield_('fix2') + yield 'fix2' await trio.sleep(0) events.append('fix2 teardown') @@ -222,7 +182,6 @@ def test_after(): 'fix2 teardown', ] """ - ) ) result = testdir.runpytest() @@ -230,30 +189,24 @@ def test_after(): result.assert_outcomes(passed=3) -def test_async_yield_fixture_with_multiple_yields( - testdir, async_yield_implementation -): +def test_async_yield_fixture_with_multiple_yields(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ @pytest.fixture - @async_generator async def fix1(): await trio.sleep(0) - await yield_('good') + yield 'good' await trio.sleep(0) - await yield_('bad') + yield 'bad' @pytest.mark.trio async def test_actual_test(fix1): pass """ - ) ) result = testdir.runpytest() @@ -263,14 +216,12 @@ async def test_actual_test(fix1): result.assert_outcomes(failed=1) -def test_async_yield_fixture_with_nursery(testdir, async_yield_implementation): +def test_async_yield_fixture_with_nursery(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ async def handle_client(stream): @@ -280,11 +231,10 @@ async def handle_client(stream): @pytest.fixture - @async_generator async def server(): async with trio.open_nursery() as nursery: listeners = await nursery.start(trio.serve_tcp, handle_client, 0) - await yield_(listeners[0]) + yield listeners[0] nursery.cancel_scope.cancel() @@ -295,7 +245,6 @@ async def test_actual_test(server): rep = await stream.receive_some(4) assert rep == b'ping' """ - ) ) result = testdir.runpytest() @@ -303,34 +252,28 @@ async def test_actual_test(server): result.assert_outcomes(passed=1) -def test_async_yield_fixture_crashed_teardown_allow_other_teardowns( - testdir, async_yield_implementation -): +def test_async_yield_fixture_crashed_teardown_allow_other_teardowns(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ setup_events = set() teardown_events = set() @pytest.fixture - @async_generator async def good_fixture(): async with trio.open_nursery() as nursery: setup_events.add('good_fixture setup') - await yield_(None) + yield None teardown_events.add('good_fixture teardown') @pytest.fixture - @async_generator async def bad_fixture(): async with trio.open_nursery() as nursery: setup_events.add('bad_fixture setup') - await yield_(None) + yield None teardown_events.add('bad_fixture teardown') raise RuntimeError('Crash during fixture teardown') @@ -352,12 +295,9 @@ def test_after(): 'good_fixture teardown', } """ - ) ) result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines( - [r'E\W+RuntimeError: Crash during fixture teardown'] - ) + result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"]) diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index c687d53..65dd911 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -58,7 +58,7 @@ def test_check_async_test_called(): result.assert_outcomes(passed=2) -@pytest.mark.xfail(reason='Raises pytest internal error so far...') +@pytest.mark.xfail(reason="Raises pytest internal error so far...") def test_sync_function_with_trio_mark(testdir): testdir.makepyfile( diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index 077861d..7bd38ed 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -35,9 +35,7 @@ def test_sync_indirect(indirect_trio_time): result = testdir.runpytest() result.assert_outcomes(passed=1, errors=2) - result.stdout.fnmatch_lines( - ["*: Trio fixtures can only be used by Trio tests*"] - ) + result.stdout.fnmatch_lines(["*: Trio fixtures can only be used by Trio tests*"]) def test_trio_fixture_with_wrong_scope_without_trio_mode(testdir): @@ -114,9 +112,7 @@ def test_whatever(async_fixture): result = testdir.runpytest() result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines( - ["*: Trio fixtures can only be used by Trio tests*"] - ) + result.stdout.fnmatch_lines(["*: Trio fixtures can only be used by Trio tests*"]) @enable_trio_mode @@ -127,14 +123,12 @@ def test_fixture_cancels_test_but_doesnt_raise(testdir, enable_trio_mode): """ import pytest import trio - from async_generator import async_generator, yield_ @pytest.fixture - @async_generator async def async_fixture(): with trio.CancelScope() as cscope: cscope.cancel() - await yield_() + yield async def test_whatever(async_fixture): @@ -169,5 +163,5 @@ async def test_whatever(mock_clock, extra_clock): result.assert_outcomes(failed=1) result.stdout.fnmatch_lines( - ["*ValueError: too many clocks spoil the broth!*"] + ["*ValueError: Expected at most one Clock in kwargs, got *"] ) diff --git a/pytest_trio/_tests/test_fixture_nursery.py b/pytest_trio/_tests/test_fixture_nursery.py index 7719fa1..503ad5e 100644 --- a/pytest_trio/_tests/test_fixture_nursery.py +++ b/pytest_trio/_tests/test_fixture_nursery.py @@ -17,6 +17,6 @@ async def server(nursery): @pytest.mark.trio async def test_try(server): stream = await trio.testing.open_stream_to_socket_listener(server) - await stream.send_all(b'ping') + await stream.send_all(b"ping") rep = await stream.receive_some(4) - assert rep == b'ping' + assert rep == b"ping" diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 8af1cdb..a180958 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -11,7 +11,6 @@ def test_fixture_basic_ordering(testdir): import pytest from pytest_trio import trio_fixture from trio.testing import Sequencer - from async_generator import async_generator, yield_ setup_events = [] teardown_events = [] @@ -21,10 +20,9 @@ def seq(): return Sequencer() @pytest.fixture - @async_generator async def leaf_fix(): setup_events.append("leaf_fix setup") - await yield_() + yield teardown_events.append("leaf_fix teardown") assert teardown_events == [ @@ -36,26 +34,24 @@ async def leaf_fix(): ] @pytest.fixture - @async_generator async def fix_concurrent_1(leaf_fix, seq): async with seq(0): setup_events.append("fix_concurrent_1 setup 1") async with seq(2): setup_events.append("fix_concurrent_1 setup 2") - await yield_() + yield async with seq(4): teardown_events.append("fix_concurrent_1 teardown 1") async with seq(6): teardown_events.append("fix_concurrent_1 teardown 2") @pytest.fixture - @async_generator async def fix_concurrent_2(leaf_fix, seq): async with seq(1): setup_events.append("fix_concurrent_2 setup 1") async with seq(3): setup_events.append("fix_concurrent_2 setup 2") - await yield_() + yield async with seq(5): teardown_events.append("fix_concurrent_2 teardown 1") async with seq(7): @@ -148,7 +144,6 @@ def test_error_collection(testdir): import pytest from pytest_trio import trio_fixture import trio - from async_generator import async_generator, yield_ test_started = False @@ -159,17 +154,15 @@ async def crash_nongen(): raise RuntimeError("crash_nongen".upper()) @trio_fixture - @async_generator async def crash_early_agen(): with trio.CancelScope(shield=True): await trio.sleep(2) raise RuntimeError("crash_early_agen".upper()) - await yield_() + yield @trio_fixture - @async_generator async def crash_late_agen(): - await yield_() + yield raise RuntimeError("crash_late_agen".upper()) async def crash(when, token): @@ -223,12 +216,11 @@ def crashyfix(nursery): crashyfix_using_manual_nursery = """ @trio_fixture - @async_generator async def crashyfix(): async with trio.open_nursery() as nursery: nursery.start_soon(crashy) with pytest.raises(trio.Cancelled): - await yield_() + yield # We should be cancelled here teardown_deadlines["crashyfix"] = trio.current_effective_deadline() """ @@ -243,7 +235,6 @@ async def crashyfix(): import pytest from pytest_trio import trio_fixture import trio - from async_generator import async_generator, yield_ teardown_deadlines = {} final_time = None @@ -283,7 +274,9 @@ def test_post(): "userfix": float("inf"), } assert final_time == 1 - """.replace("CRASHYFIX_HERE", crashyfix) + """.replace( + "CRASHYFIX_HERE", crashyfix + ) ) result = testdir.runpytest() @@ -298,27 +291,25 @@ def test_complex_cancel_interaction_regression(testdir): """ import pytest import trio - from async_generator import asynccontextmanager, async_generator, yield_ + from contextlib import asynccontextmanager async def die_soon(): raise RuntimeError('oops'.upper()) @asynccontextmanager - @async_generator async def async_finalizer(): try: - await yield_() + yield finally: await trio.sleep(0) @pytest.fixture - @async_generator async def fixture(nursery): async with trio.open_nursery() as nursery1: async with async_finalizer(): async with trio.open_nursery() as nursery2: nursery2.start_soon(die_soon) - await yield_() + yield nursery1.cancel_scope.cancel() @pytest.mark.trio @@ -329,9 +320,7 @@ async def test_try(fixture): result = testdir.runpytest() result.assert_outcomes(passed=0, failed=1) - result.stdout.fnmatch_lines_random([ - "*OOPS*", - ]) + result.stdout.fnmatch_lines_random(["*OOPS*"]) # Makes sure that diff --git a/pytest_trio/_tests/test_hypothesis_interaction.py b/pytest_trio/_tests/test_hypothesis_interaction.py index 30a37db..75aa9f7 100644 --- a/pytest_trio/_tests/test_hypothesis_interaction.py +++ b/pytest_trio/_tests/test_hypothesis_interaction.py @@ -1,8 +1,9 @@ import pytest import trio from trio.tests.test_scheduler_determinism import ( - scheduler_trace, test_the_trio_scheduler_is_not_deterministic, - test_the_trio_scheduler_is_deterministic_if_seeded + scheduler_trace, + test_the_trio_scheduler_is_not_deterministic, + test_the_trio_scheduler_is_deterministic_if_seeded, ) from hypothesis import given, settings, strategies as st @@ -29,7 +30,7 @@ async def test_mark_outer(n): @our_settings -@pytest.mark.parametrize('y', [1, 2]) +@pytest.mark.parametrize("y", [1, 2]) @given(x=st.none()) @pytest.mark.trio async def test_mark_and_parametrize(x, y): diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index 7a4cb0f..508b2f0 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -3,12 +3,12 @@ @pytest.fixture def sync_fix(): - return 'sync_fix' + return "sync_fix" @pytest.mark.trio async def test_single_sync_fixture(sync_fix): - assert sync_fix == 'sync_fix' + assert sync_fix == "sync_fix" def test_single_yield_fixture(testdir): @@ -139,6 +139,4 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines( - [r'E\W+RuntimeError: Crash during fixture teardown'] - ) + result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"]) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index f7bf61f..19efdc1 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -56,9 +56,7 @@ def run(*args, **kwargs): def test_trio_mode_and_qtrio_run_configuration(testdir): - testdir.makefile( - ".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" - ) + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n") testdir.makepyfile(qtrio=qtrio_text) @@ -141,9 +139,7 @@ async def test(): def test_closest_explicit_run_wins(testdir): - testdir.makefile( - ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = trio\n" - ) + testdir.makefile(".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = trio\n") testdir.makepyfile(qtrio=qtrio_text) test_text = """ @@ -163,9 +159,7 @@ async def test(): def test_ini_run_wins_with_blank_marker(testdir): - testdir.makefile( - ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" - ) + testdir.makefile(".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n") testdir.makepyfile(qtrio=qtrio_text) test_text = """ diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 8ddb107..34e64a8 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,19 +1,15 @@ """pytest-trio implementation.""" from functools import wraps, partial -import sys from traceback import format_exception from collections.abc import Coroutine, Generator -from inspect import iscoroutinefunction, isgeneratorfunction +from contextlib import asynccontextmanager +from inspect import isasyncgen, isasyncgenfunction, iscoroutinefunction import contextvars import outcome import pytest import trio from trio.abc import Clock, Instrument from trio.testing import MockClock -from async_generator import ( - async_generator, yield_, asynccontextmanager, isasyncgen, - isasyncgenfunction -) ################################################################ # Basic setup @@ -51,9 +47,8 @@ def pytest_addoption(parser): def pytest_configure(config): # So that it shows up in 'pytest --markers' output: config.addinivalue_line( - "markers", "trio: " - "mark the test as an async trio test; " - "it will be run using trio.run" + "markers", + "trio: mark the test as an async trio test; it will be run using trio.run", ) @@ -61,7 +56,7 @@ def pytest_configure(config): def pytest_exception_interact(node, call, report): if issubclass(call.excinfo.type, trio.MultiError): # TODO: not really elegant (pytest cannot output color with this hack) - report.longrepr = ''.join(format_exception(*call.excinfo._excinfo)) + report.longrepr = "".join(format_exception(*call.excinfo._excinfo)) ################################################################ @@ -195,13 +190,12 @@ def register_and_collect_dependencies(self): return deps @asynccontextmanager - @async_generator async def _fixture_manager(self, test_ctx): __tracebackhide__ = True try: async with trio.open_nursery() as nursery_fixture: try: - await yield_(nursery_fixture) + yield nursery_fixture finally: nursery_fixture.cancel_scope.cancel() except BaseException as exc: @@ -343,19 +337,17 @@ def decorator(fn): @wraps(fn) def wrapper(**kwargs): __tracebackhide__ = True - clocks = [c for c in kwargs.values() if isinstance(c, Clock)] + clocks = {k: c for k, c in kwargs.items() if isinstance(c, Clock)} if not clocks: clock = None elif len(clocks) == 1: - clock = clocks[0] + clock = list(clocks.values())[0] else: - raise ValueError("too many clocks spoil the broth!") - instruments = [ - i for i in kwargs.values() if isinstance(i, Instrument) - ] - return run( - partial(fn, **kwargs), clock=clock, instruments=instruments - ) + raise ValueError( + f"Expected at most one Clock in kwargs, got {clocks!r}" + ) + instruments = [i for i in kwargs.values() if isinstance(i, Instrument)] + return run(partial(fn, **kwargs), clock=clock, instruments=instruments) return wrapper @@ -369,7 +361,7 @@ def _trio_test_runner_factory(item, testfunc=None): testfunc = item.obj for marker in item.iter_markers("trio"): - maybe_run = marker.kwargs.get('run') + maybe_run = marker.kwargs.get("run") if maybe_run is not None: run = maybe_run break @@ -377,15 +369,13 @@ def _trio_test_runner_factory(item, testfunc=None): # no marker found that explicitly specifiers the runner so use config run = choose_run(config=item.config) - if getattr(testfunc, '_trio_test_runner_wrapped', False): + if getattr(testfunc, "_trio_test_runner_wrapped", False): # We have already wrapped this, perhaps because we combined Hypothesis # with pytest.mark.parametrize return testfunc if not iscoroutinefunction(testfunc): - pytest.fail( - 'test function `%r` is marked trio but is not async' % item - ) + pytest.fail("test function `%r` is marked trio but is not async" % item) @_trio_test(run=run) async def _bootstrap_fixtures_and_run_test(**kwargs): @@ -393,10 +383,7 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): test_ctx = TrioTestContext() test = TrioFixture( - "".format(testfunc.__name__), - testfunc, - kwargs, - is_test=True + "".format(testfunc.__name__), testfunc, kwargs, is_test=True ) contextvars_ctx = contextvars.copy_context() @@ -435,16 +422,15 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): if item.get_closest_marker("trio") is not None: - if hasattr(item.obj, 'hypothesis'): + if hasattr(item.obj, "hypothesis"): # If it's a Hypothesis test, we go in a layer. item.obj.hypothesis.inner_test = _trio_test_runner_factory( item, item.obj.hypothesis.inner_test ) - elif getattr(item.obj, 'is_hypothesis_test', - False): # pragma: no cover + elif getattr(item.obj, "is_hypothesis_test", False): # pragma: no cover pytest.fail( - 'test function `%r` is using Hypothesis, but pytest-trio ' - 'only works with Hypothesis 3.64.0 or later.' % item + "test function `%r` is using Hypothesis, but pytest-trio " + "only works with Hypothesis 3.64.0 or later." % item ) else: item.obj = _trio_test_runner_factory(item) @@ -463,8 +449,7 @@ def trio_fixture(func): def _is_trio_fixture(func, coerce_async, kwargs): if getattr(func, "_force_trio_fixture", False): return True - if (coerce_async and - (iscoroutinefunction(func) or isasyncgenfunction(func))): + if coerce_async and (iscoroutinefunction(func) or isasyncgenfunction(func)): return True if any(isinstance(value, TrioFixture) for value in kwargs.values()): return True @@ -472,16 +457,13 @@ def _is_trio_fixture(func, coerce_async, kwargs): def handle_fixture(fixturedef, request, force_trio_mode): - is_trio_test = (request.node.get_closest_marker("trio") is not None) + is_trio_test = request.node.get_closest_marker("trio") is not None if force_trio_mode: is_trio_mode = True else: is_trio_mode = request.node.config.getini("trio_mode") - coerce_async = (is_trio_test or is_trio_mode) - kwargs = { - name: request.getfixturevalue(name) - for name in fixturedef.argnames - } + coerce_async = is_trio_test or is_trio_mode + kwargs = {name: request.getfixturevalue(name) for name in fixturedef.argnames} if _is_trio_fixture(fixturedef.func, coerce_async, kwargs): if request.scope != "function": raise RuntimeError("Trio fixtures must be function-scope") @@ -522,11 +504,12 @@ def choose_run(config): run = trio.run elif run_string == "qtrio": import qtrio + run = qtrio.run else: raise ValueError( - f"{run_string!r} not valid for 'trio_run' config." + - " Must be one of: trio, qtrio" + f"{run_string!r} not valid for 'trio_run' config." + + " Must be one of: trio, qtrio" ) return run diff --git a/setup.py b/setup.py index 054ffca..ecf6f8b 100644 --- a/setup.py +++ b/setup.py @@ -14,21 +14,19 @@ author_email="emmanuel.leblond@gmail.com", license="MIT -or- Apache License 2.0", packages=find_packages(), - entry_points={'pytest11': ['trio = pytest_trio.plugin']}, + entry_points={"pytest11": ["trio = pytest_trio.plugin"]}, install_requires=[ - "trio >= 0.15.0", - "async_generator >= 1.9", - "outcome", - # For node.get_closest_marker - "pytest >= 3.6" + "trio >= 0.22.0", # for ExceptionGroup support + "outcome >= 1.1.0", + "pytest >= 7.2.0", # for ExceptionGroup support ], keywords=[ - 'async', - 'pytest', - 'testing', - 'trio', + "async", + "pytest", + "testing", + "trio", ], - python_requires=">=3.6", + python_requires=">=3.7", classifiers=[ "License :: OSI Approved :: MIT License", "License :: OSI Approved :: Apache Software License", @@ -36,6 +34,11 @@ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Networking", diff --git a/test-requirements.txt b/test-requirements.txt index c8280fc..b1d1509 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,3 @@ -pytest==6.1.2 -pytest-cov==2.10.1 -hypothesis==5.47.0 +pytest==7.2.0 +pytest-cov==4.0.0 +hypothesis==6.56.4