Skip to content

refactor: modernize typing and defaults across core API#1299

Open
Sachin-Bhat wants to merge 3 commits intosparckles:mainfrom
Sachin-Bhat:refactor/pep_604_compliance
Open

refactor: modernize typing and defaults across core API#1299
Sachin-Bhat wants to merge 3 commits intosparckles:mainfrom
Sachin-Bhat:refactor/pep_604_compliance

Conversation

@Sachin-Bhat
Copy link
Copy Markdown

@Sachin-Bhat Sachin-Bhat commented Feb 3, 2026

Description

This PR fixes #1298

Summary

This PR does the following:

  • Modernize typing across core modules and stubs (PEP 604 unions, collections.abc.Callable, StrEnum, and TypeAlias usage).
  • Remove mutable default arguments by initializing Config and DependencyMap lazily in constructors.
  • Tighten HTTP method handling and router response formatting, including explicit validation for string methods and guarding tuple responses against streaming.
  • Minor cleanup/formatting touches and small robustness improvements (e.g., optional allow_connection_pickling).

PR Checklist

Please ensure that:

  • The PR contains a descriptive title
  • The PR contains a descriptive summary of the changes
  • You build and test your changes before submitting a PR.
  • You have added relevant documentation
  • You have added relevant tests. We prefer integration tests wherever possible

Pre-Commit Instructions:

Summary by CodeRabbit

  • Bug Fixes

    • Prevented duplicate routes by method and normalized endpoint to avoid collisions.
  • Refactor

    • Modernized public type annotations across the framework for consistency.
    • Replaced mutable defaults with explicit None-default handling for safer initialization.
    • Broadened optional parameter handling for many public APIs to improve ergonomics.
  • Tests

    • Added WebSocket test scaffolding and a global exception handler for test coverage.
  • Style

    • Minor formatting/whitespace cleanup in an SSE example.

Signed-off-by: Sachin Bhat <sachubhat17@gmail.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
robyn Ready Ready Preview, Comment Feb 3, 2026 9:28am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

Modernizes type annotations to Python 3.10+ (PEP 604) across the codebase, replaces legacy typing generics with built-ins, applies lazy default initialization for mutable defaults, adds route deduplication, changes dependency merge behavior, and adjusts a few runtime checks (subprocess capture, async generator detection). Mostly signature and typing updates.

Changes

Cohort / File(s) Summary
Core typing modernizations
robyn/ai.py, robyn/authentication.py, robyn/cli.py, robyn/logger.py, robyn/types.py, robyn/processpool.py
Convert legacy typing (Optional/Union/List/Dict) to PEP 604 unions and built-in generics (`X
Framework core & routing
robyn/__init__.py, robyn/router.py, robyn/ws.py, robyn/templating.py
Use None-defaults for Config/DependencyMap and lazily initialize; update many public signatures to new typing; add route deduplication (raise on duplicate) and normalize route/middleware endpoint typing.
Dependency injection
robyn/dependency_injection.py
merge_dependencies now unconditionally assigns global dependency entries into the target (overwrites existing keys) rather than only adding missing keys.
Responses & SSE/streaming
robyn/responses.py, examples/sse_example.py
Modernize Streaming/SSEResponse and SSEMessage typings; tighten async-generator detection; examples/sse_example.py changes are whitespace/formatting only.
OpenAPI & MCP
robyn/openapi.py, robyn/mcp.py
Add Protocol/TypeAlias helpers, modernize component/metadata typing, refine schema/type resolution and error propagation; update MCP dataclasses and handler signatures.
Reloader & subprocesses
robyn/reloader.py, robyn/processpool.py
Typing modernized; subprocess.run calls switched to capture_output=True; process attribute typings updated; Rust compile/clean signatures modernized.
Public typing surface (.pyi / stubs)
robyn/robyn.pyi, robyn/mcp.py, robyn/responses.py
Update public stubs and dataclass signatures (StrEnum for types/methods, PEP 604 unions), expanding/altering the public typing surface.
Tests / integration
integration_tests/base_routes.py
Add a global exception handler, new WebSocket test endpoints, and update nullable field typings in test models.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • VishnuSanal
  • sansyrox

Poem

"I hopped through code with carrot-bright cheer,
Replacing Optionals so types are clear,
Pipes for unions, defaults set to None,
Routes deduplicate — one by one. 🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: modernizing typing across the core API with PEP 604 and removing mutable defaults.
Description check ✅ Passed The description is comprehensive, includes the linked issue (#1298), provides a clear summary of changes, and has a complete PR checklist with all boxes checked.
Linked Issues check ✅ Passed The PR fully addresses issue #1298 by implementing PEP 604 unions, using collections.abc.Callable, StrEnum/TypeAlias, builtin types, and removing mutable defaults across all core modules.
Out of Scope Changes check ✅ Passed All changes align with the modernization objectives. The SSE example formatting, OpenAPI schema adjustments, and router response validation are directly related to the typing modernization and code quality improvements.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In `@robyn/openapi.py`:
- Around line 108-116: The typed fields (schemas, responses, parameters,
examples, requestBodies, securitySchemes, links, callbacks, pathItems) are
annotated as dict[str, dict] | None but use default_factory=dict, creating empty
dicts rather than None; update the annotations to be non-optional (remove "|
None") so they read dict[str, dict] and keep default_factory=dict for each
field, or alternatively change the defaults to default=None if you really want
them nullable—adjust any code that expects None accordingly.

In `@robyn/templating.py`:
- Around line 11-12: The abstract `@abstractmethod` on __init__ makes all
subclasses abstract and uninstantiable; remove the `@abstractmethod` decorator
from __init__ in templating.py and provide a concrete, flexible initializer (def
__init__(self, *args, **kwargs): pass or call super().__init__(*args, **kwargs))
so subclasses can inherit a default constructor while still allowing overrides;
update the __init__ definition (symbol __init__) to be non-abstract and keep the
*args/**kwargs signature for compatibility.
🧹 Nitpick comments (3)
robyn/reloader.py (1)

119-119: Add type parameter to list for consistency.

This uses bare list without a type parameter, while the rest of the file uses list[str] (e.g., compile_rust_files returns list[str], clean_rust_binaries takes list[str]). For consistency and better type safety, this should be list[str].

♻️ Proposed fix
-        self.built_rust_binaries: list = []  # Keep track of the built rust binaries
+        self.built_rust_binaries: list[str] = []  # Keep track of the built rust binaries
robyn/logger.py (1)

68-76: Consider aligning debug method signature with the other logging methods.

The debug method uses color: Colors = Colors.BLUE without the | None option, while error, warn, and info all use Colors | None. If this is intentional (debug always requires a color), that's fine. Otherwise, consider updating for consistency.

robyn/__init__.py (1)

197-207: Consider initializing _added_routes in __init__.

The hasattr/setattr pattern for _added_routes works but is unconventional. Initializing in __init__ would be cleaner and more explicit.

♻️ Proposed refactor

In BaseRobyn.__init__:

         self.authentication_handler: AuthenticationHandler | None = None
         self.included_routers: list[SubRouter] = []
         self._mcp_app: MCPApp | None = None
+        self._added_routes: set[str] = set()

Then in add_route:

-        # Check if this exact route (method + normalized_endpoint) already exists
-        route_key = f"{route_type}:{normalized_endpoint}"
-        if not hasattr(self, "_added_routes"):
-            self._added_routes = set()
-
-        if route_key in self._added_routes:
+        # Check if this exact route (method + normalized_endpoint) already exists
+        route_key = f"{route_type}:{normalized_endpoint}"
+        if route_key in self._added_routes:

Signed-off-by: Sachin Bhat <sachubhat17@gmail.com>
Copy link
Copy Markdown

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
robyn/__init__.py (1)

33-67: ⚠️ Potential issue | 🟡 Minor

Treat whitespace-only endpoints as blank.
Right now " " normalizes to "/", which can accidentally register root middleware instead of global. Strip early and honor treat_empty_as_root for blank/whitespace values.

🛠️ Suggested fix
-    if endpoint is None or (not endpoint and not treat_empty_as_root):
-        return None
-
-    # Remove trailing slashes
-    endpoint = endpoint.strip().rstrip("/")
-
-    # Handle empty result
-    if not endpoint:
-        return "/"
+    if endpoint is None:
+        return None
+
+    endpoint = endpoint.strip()
+    if not endpoint:
+        return "/" if treat_empty_as_root else None
+
+    endpoint = endpoint.rstrip("/")
Based on learnings: In robyn/__init__.py, for `before_request()` and `after_request()` middleware decorators: blank string `""` or no argument (None) registers global middleware; only explicit `"/"` registers middleware for the root endpoint. This is the intended behavior of the framework.
🤖 Fix all issues with AI agents
In `@robyn/openapi.py`:
- Around line 408-422: The anyOf generation for Union/Optional currently uses
get_openapi_type (which only handles primitives), causing complex members (e.g.,
list[str] or custom models) to be rendered as {"type":"string"}; update the
Union handling in the block that inspects origin in (typing.Union,
types.UnionType) so each arg is converted via self.get_schema_object (or calls
self.get_schema_object(f"{parameter}_union_member", arg) for non-None args)
instead of using get_openapi_type, while keeping {"type":"null"} for NoneType;
ensure the resulting list is assigned to properties["anyOf"] and then returned.

Signed-off-by: Sachin Bhat <sachubhat17@gmail.com>
Comment on lines -799 to -806
# ==== Exception Handling ====


@app.exception
def handle_exception(error):
return Response(status_code=500, description=f"error msg: {error}", headers={})


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let's keep this


from integration_tests.subroutes import di_subrouter, static_router, sub_router
from robyn import Headers, Request, Response, Robyn, SSEMessage, SSEResponse, WebSocket, WebSocketConnector, jsonify, serve_file, serve_html
from robyn import (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why?

Comment on lines +182 to +187
route_key = route_type.upper()
try:
route_type = getattr(HttpMethod, route_key)
except AttributeError as exc:
raise ValueError(f"Unsupported HTTP method: {route_type}") from exc
route_type = cast(HttpMethod, route_type)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I prefer the original way. Could you revert to that?

return s.connect_ex(("localhost", port)) == 0
except Exception:
raise Exception(f"Invalid port number: {port}")
raise Exception(f"Invalid port number: {port}") from None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please keep original

const: bool = False,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["get"],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it makes sense to keep this.

endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["post"],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same

endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["put"],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same

endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["delete"],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same

endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["patch"],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same

endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["head"],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same

endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["connect"],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same

This method iterates through the dependencies of this DependencyMap and applies them to the
target router's DependencyMap, overriding any existing keys.
"""
for dep_key in self.get_global_dependencies():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is faster


if config_path.exists():
with open(config_path, "r") as f:
with open(config_path) as f:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why?

except TypeError as e:
# Handle parameter mismatch errors
raise MCPError(-32603, f"Handler parameter error: {str(e)}")
raise MCPError(-32603, f"Handler parameter error: {str(e)}") from e
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why?

@sansyrox
Copy link
Copy Markdown
Member

sansyrox commented Feb 3, 2026

Hey @Sachin-Bhat 👋

Thank you for the PR, and apologies for the gazillion pings. I've left a few comments.

@sansyrox sansyrox force-pushed the main branch 3 times, most recently from 5fb08e0 to 0472e42 Compare February 15, 2026 06:39
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.

Modernize typing in the core python API

2 participants