Skip to content

test: add ASGI unit tests and Python 2.7 collection guard#83

Merged
joamag merged 7 commits intomasterfrom
test/asgi
Feb 4, 2026
Merged

test: add ASGI unit tests and Python 2.7 collection guard#83
joamag merged 7 commits intomasterfrom
test/asgi

Conversation

@joamag
Copy link
Contributor

@joamag joamag commented Feb 4, 2026

Add 13 unit tests for the ASGI integration layer covering environ building, body reading, start response, sender, lifespan handling and invalid scope rejection. Add conftest.py to skip ASGI test collection on Python < 3.5 where async syntax causes SyntaxError.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed ASGI lifespan startup/shutdown failure handling to properly emit failure events per specification
    • Corrected query string decoding to use latin1 encoding for WSGI compliance
    • Improved handling of byte string encoding in ASGI response processing
  • Tests

    • Added comprehensive test coverage for ASGI environ building, body reading, response handling, and lifespan events

Add 13 unit tests for the ASGI integration layer covering environ
building, body reading, start response, sender, lifespan handling
and invalid scope rejection. Add conftest.py to skip ASGI test
collection on Python < 3.5 where async syntax causes SyntaxError.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 4, 2026 17:20
@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

Warning

Rate limit exceeded

@joamag has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 1 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR adds comprehensive ASGI unit tests for the Appier framework and refines ASGI handling logic. Changes include a new test suite with ASGI-WSGI bridging tests, lifespan startup/shutdown failure handling per ASGI specification, and query string decoding corrected from ASCII to Latin-1 per PEP 3333.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md
Added entries documenting new ASGI unit tests, changed ASGI response encoding behavior, and fixed lifespan failure event handling and query string decoding.
ASGI Implementation
src/appier/asgi.py
Modified lifespan manager to wrap startup/shutdown handling in try/except blocks, emit lifespan.startup.failed and lifespan.shutdown.failed events on errors, replaced asyncio.create_task with asyncio.ensure_future in start response, and changed QUERY_STRING decoding from ASCII to Latin-1.
Test Module Structure
src/appier/test/asgi.py, src/appier/test_py3/__init__.py
Added new test module with conditional Python 3.5+ guard and test package initializer with metadata declarations.
ASGI Test Suite
src/appier/test_py3/_asgi_test.py
Comprehensive test suite covering environ building, body reading, start response handling, sender behavior, lifespan events with failure scenarios, and query string decoding validation.

Sequence Diagram(s)

sequenceDiagram
    participant Receiver as ASGI Receiver
    participant Manager as Lifespan Manager
    participant Sender as ASGI Sender

    Receiver->>Manager: lifespan.startup event
    rect rgba(100, 200, 100, 0.5)
        alt Startup Success
            Manager->>Manager: start() app
            Manager->>Sender: lifespan.startup.complete
        else Startup Failure
            Manager->>Manager: exception caught
            Manager->>Sender: lifespan.startup.failed
        end
    end

    Receiver->>Manager: lifespan.shutdown event
    rect rgba(100, 150, 200, 0.5)
        alt Shutdown Success
            Manager->>Manager: stop() app
            Manager->>Sender: lifespan.shutdown.complete
        else Shutdown Failure
            Manager->>Manager: exception caught
            Manager->>Sender: lifespan.shutdown.failed
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops through ASGI with care,
Tests woven with Latin-1 flair,
When startup fails, we know the way,
Events bouncing, come what may,
Error handling, sturdy and fair! 🎯

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding ASGI unit tests and a Python collection guard for test discovery.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test/asgi

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/appier/test/conftest.py`:
- Around line 33-34: The version-check logic using sys.version_info[0] < 3 or
sys.version_info[1] < 5 is incorrect for future major versions; update the
condition to a tuple comparison such as sys.version_info[:2] < (3, 5) so that
the check correctly evaluates versions (this affects the existing collect_ignore
append("asgi.py") branch referencing sys.version_info and collect_ignore).
🧹 Nitpick comments (2)
src/appier/test/conftest.py (1)

5-5: Copyright year may need updating.

The copyright shows 2008-2024, but we're currently in 2026. Consider updating to 2008-2026 for consistency.

src/appier/test/asgi.py (1)

5-5: Copyright year may need updating.

Same as conftest.py, the copyright shows 2008-2024 but should likely be 2008-2026.

Comment on lines 33 to 34
if sys.version_info[0] < 3 or sys.version_info[1] < 5:
collect_ignore.append("asgi.py")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the conftest.py file exists and read it
cat -n src/appier/test/conftest.py

Repository: hivesolutions/appier

Length of output: 1538


🏁 Script executed:

# Check for asgi.py in the test directory and elsewhere
fd -t f "asgi.py" src/appier/

Repository: hivesolutions/appier

Length of output: 126


🏁 Script executed:

# Search for imports of asgi module
rg "from.*asgi import|import.*asgi" src/appier/ --glob "!**/asgi.py"

Repository: hivesolutions/appier

Length of output: 105


Fix version check logic for future Python compatibility.

The condition sys.version_info[0] < 3 or sys.version_info[1] < 5 uses OR logic which fails for hypothetical Python 4.x versions with minor < 5 (e.g., Python 4.1 would incorrectly exclude asgi.py tests). Use tuple comparison for robust version checking:

-if sys.version_info[0] < 3 or sys.version_info[1] < 5:
+if sys.version_info[:2] < (3, 5):
     collect_ignore.append("asgi.py")
🤖 Prompt for AI Agents
In `@src/appier/test/conftest.py` around lines 33 - 34, The version-check logic
using sys.version_info[0] < 3 or sys.version_info[1] < 5 is incorrect for future
major versions; update the condition to a tuple comparison such as
sys.version_info[:2] < (3, 5) so that the check correctly evaluates versions
(this affects the existing collect_ignore append("asgi.py") branch referencing
sys.version_info and collect_ignore).

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds unit test coverage for the ASGI integration layer while preventing pytest from collecting async/await tests on unsupported Python versions.

Changes:

  • Added 13 unit tests validating ASGI environ building, body reading, start response behavior, sender behavior, lifespan handling, and invalid scope rejection.
  • Added a pytest conftest.py collection guard to avoid SyntaxError on interpreters that can’t parse async/await.
  • Updated CHANGELOG.md to record the added ASGI tests and collection guard.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/appier/test/conftest.py Adds pytest collection ignore logic for ASGI test module on older Python versions.
src/appier/test/asgi.py Introduces a new unittest suite covering ASGI helper methods and lifespan handling.
CHANGELOG.md Documents the addition of ASGI unit tests and the collection guard.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

# ignores test modules that use async/await syntax and would
# cause a SyntaxError when collected under Python 2.7
collect_ignore = []
if sys.version_info[0] < 3 or sys.version_info[1] < 5:
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version guard is using sys.version_info[0] < 3 or sys.version_info[1] < 5, which can misclassify versions (e.g., Python 4.0 would still match because 0 < 5). Use a tuple comparison like sys.version_info < (3, 5) (or an explicit major/minor check) so the ignore logic remains correct for future versions.

Suggested change
if sys.version_info[0] < 3 or sys.version_info[1] < 5:
if sys.version_info < (3, 5):

Copilot uses AI. Check for mistakes.
Comment on lines 31 to 40
import asyncio
import unittest

import appier


class ASGITest(unittest.TestCase):
def setUp(self):
self.app = appier.App()

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests exercise ASGI helpers that currently call asyncio.create_task (see src/appier/asgi.py), which is unavailable on Python 3.5/3.6. Since the repo CI matrix includes 3.5 and 3.6, this file will likely fail at runtime on those versions unless you either (a) skip the whole module/class when asyncio.create_task is missing, or (b) update the ASGI implementation to fall back to asyncio.ensure_future/loop.create_task and keep the tests enabled.

Copilot uses AI. Check for mistakes.

*
* ASGI unit tests covering environ building, body reading, start response, sender, lifespan and invalid scope handling
* Test conftest to exclude ASGI tests on Python < 3.5
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog entry says ASGI tests are excluded on Python < 3.5, but the new ASGI tests will still run on 3.5/3.6 (and currently the ASGI implementation uses asyncio.create_task, which isn’t available there). After deciding whether to skip on <3.7 or add a compatibility fallback, please update this line to reflect the actual supported/covered versions so the changelog doesn’t mislead.

Suggested change
* Test conftest to exclude ASGI tests on Python < 3.5
* Test conftest to exclude ASGI tests on Python < 3.7

Copilot uses AI. Check for mistakes.
joamag and others added 5 commits February 4, 2026 17:32
…mpat

Move ASGI test implementation from appier.test._asgi_async to
appier._asgi_test so setuptools' test loader does not discover and
import it on Python 2.7 (where async syntax causes SyntaxError).
Remove conftest.py as it only affected pytest which requires 3.7+.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add try/except to asgi_lifespan() so startup/shutdown exceptions
send lifespan.startup.failed or lifespan.shutdown.failed events
with a message field, as required by the ASGI lifespan spec.

Change query_string decoding from ASCII to latin1 in _build_environ()
to comply with PEP 3333 and avoid UnicodeDecodeError on non-ASCII bytes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/appier/test_py3/_asgi_test.py`:
- Around line 1-33: The file src/appier/test_py3/_asgi_test.py is failing Black
formatting; reformat the module with Black (e.g., black . or black
src/appier/test_py3/_asgi_test.py) so the imports and top-level metadata (the
__author__, __copyright__, __license__ declarations and the import block: io,
asyncio, unittest) conform to the project's Black settings, then stage the
formatted file and update the PR.
🧹 Nitpick comments (1)
src/appier/asgi.py (1)

141-171: Avoid swallowing task cancellation in lifespan error handling.

Catching Exception will intercept asyncio.CancelledError on some Python versions, preventing proper cancellation. Re-raise cancellation before emitting failed events.

Proposed adjustment
             if event["type"] == "lifespan.startup":
                 try:
                     if not self.is_started():
                         self.start()
                     await send(dict(type="lifespan.startup.complete"))
-                except Exception as exception:
+                except asyncio.CancelledError:
+                    raise
+                except Exception as exception:
                     await send(
                         dict(
                             type="lifespan.startup.failed",
                             message=str(exception),
                         )
                     )
                     break

             elif event["type"] == "lifespan.shutdown":
                 try:
                     if not self.is_stopped():
                         self.stop()
                     await send(dict(type="lifespan.shutdown.complete"))
-                except Exception as exception:
+                except asyncio.CancelledError:
+                    raise
+                except Exception as exception:
                     await send(
                         dict(
                             type="lifespan.shutdown.failed",
                             message=str(exception),
                         )
                     )

Comment on lines +1 to +33
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Hive Appier Framework
# Copyright (c) 2008-2024 Hive Solutions Lda.
#
# This file is part of Hive Appier Framework.
#
# Hive Appier Framework is free software: you can redistribute it and/or modify
# it under the terms of the Apache License as published by the Apache
# Foundation, either version 2.0 of the License, or (at your option) any
# later version.
#
# Hive Appier Framework is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Apache License for more details.
#
# You should have received a copy of the Apache License along with
# Hive Appier Framework. If not, see <http://www.apache.org/licenses/>.

__author__ = "João Magalhães <joamag@hive.pt>"
""" The author(s) of the module """

__copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
""" The copyright for the module """

__license__ = "Apache License, Version 2.0"
""" The license for the module """

import io
import asyncio
import unittest
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Black formatting check is failing for this file.

CI indicates one file needs Black formatting—please run Black on this module.

As per coding guidelines, Format Python code with Black before committing.

🤖 Prompt for AI Agents
In `@src/appier/test_py3/_asgi_test.py` around lines 1 - 33, The file
src/appier/test_py3/_asgi_test.py is failing Black formatting; reformat the
module with Black (e.g., black . or black src/appier/test_py3/_asgi_test.py) so
the imports and top-level metadata (the __author__, __copyright__, __license__
declarations and the import block: io, asyncio, unittest) conform to the
project's Black settings, then stage the formatted file and update the PR.

@joamag joamag merged commit 8770ee1 into master Feb 4, 2026
49 checks passed
@joamag joamag deleted the test/asgi branch February 4, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants