Visitar URL original
Reject unsupported native keywords before argument binding by widehyo1 · Pull Request #8957 · RustPython/RustPython · GitHub
Skip to content

Reject unsupported native keywords before argument binding - #8957

Open
widehyo1 wants to merge 4 commits into
RustPython:mainfrom
widehyo1:fix/native-keyword-dispatch
Open

widehyo1 wants to merge 4 commits into
RustPython:mainfrom
widehyo1:fix/native-keyword-dispatch

Conversation

@widehyo1

@widehyo1 widehyo1 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Reject unsupported keyword arguments before native argument binding and
conversion, matching CPython's dispatch order. For example, id(obj=1) now
raises TypeError: id() takes no keyword arguments instead of a positional
argument binding error.

The existing id(1, **{'foo': 1}) doctest in test_extcall now passes, so
remove its TODO and EXPECTED_FAILURE marker without changing the call or
expected output.

Details

  • Derive KeywordDispatch policies at registration time from
    FromArgs::PARAMS for macro-registered functions and methods, including
    nested parameter metadata, without explicit no_keywords annotations.
    This policy is independent of flags inferred from Rust signatures.
  • Forward keyword-capable arguments to the binder. Raw FuncArgs functions
    such as min and max retain validation in their function bodies.
  • Check nonempty kwargs/kwnames before binding and conversion. Unbound
    method descriptors first validate positional self presence and type.
    Empty keyword dictionaries remain accepted.
  • Derive C-API policies from actual ml_flags, including the unqualified
    diagnostic name used by legacy METH_VARARGS functions.
  • Preserve the original dynamic registration argument order and add a
    keyword_dispatch argument to Context::new_method_def.
  • Reuse SimpleItemMeta for function attributes and remove the redundant
    FunctionItemMeta type.
  • Consolidate Python behavior regressions in
    extra_tests/snippets/syntax_function_args.py, plus a C-API regression
    for keyword rejection before NOARGS arity validation.

Automatic keyword dispatch applies to macro-registered functions and
methods. Constructors, slot wrappers, and keyword-capable parsers are not
rewritten. Positional-only arity diagnostics are outside this change.

Context::new_method_def now takes
(name, callable, flags, doc, keyword_dispatch); embedding callers using
the previous signature need to supply the new policy argument. Direct PyMethodDef
struct initializers also need the new keyword_dispatch field.

Testing

Latest validation: temp/extcall/validation/inference-20261006-072918/.

Passed:

  • Workspace tests and C-API tests (cargo test from crates/capi).
  • Workspace and C-API Clippy with the separate C-API configuration.
  • cargo check -p rustpython-vm --no-default-features.
  • Consolidated keyword regressions in syntax_function_args.py on
    RustPython and through the CPython/RustPython snippet harness.
  • test_call, test_extcall, test_builtin, and test_list via the release
    interpreter: 402 tests run, 198 skipped; all four modules passed.
  • cargo fmt --all --check and git diff --check.

The full extra_tests pytest run reported 477 passed, 7 failed. Six
failures were on CPython (builtin_memoryview, builtin_signature,
stdlib_atexit, stdlib_gc, stdlib_struct, and stdlib_unicode_shared).
The RustPython failure was stdlib_sqlite: _sqlite3 was unavailable in
the default build. The consolidated keyword regressions passed on both interpreters.

AI assistance: Codex:GPT-6

Summary by CodeRabbit

  • Bug Fixes
    • Built-in functions and methods now consistently reject unsupported keyword arguments with clearer errors.
    • Method calls now require a positional instance argument, preventing calls that omit self.
    • Improved argument validation and error precedence across built-in functions, while preserving supported keyword handling.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions github-actions Bot added the z-ca-2026 Tag to track Contribution Academy 2026 label Oct 4, 2026
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: RustPython/RustPython/.coderabbit.yml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8951a077-965f-412b-a41e-dc77c17f3fa9
📥 Commits

Reviewing files that changed from the base of the PR and between 3624faf and a0c0223.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_extcall.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/vm/src/builtins/builtin_func.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

C-API method definitions now select keyword dispatch from method flags. Native function and method descriptor call paths reject unsupported keyword arguments. Method descriptors require a valid positional receiver. Tests cover these checks and keyword handling across built-in functions and methods.

Changes

Native keyword and receiver validation

Layer / File(s) Summary
Set keyword dispatch in method definitions
crates/capi/src/methodobject.rs
Method definitions select keyword dispatch from method flags and pass it to each calling-convention branch. A test checks that a method without keyword support rejects nonempty keywords and accepts an empty keyword dictionary.
Reject unsupported native function keywords
crates/vm/src/builtins/builtin_func.rs
Native function call paths reject nonempty keyword arguments when the function lacks the KEYWORDS flag. Error messages use the function name or qualified name, with a module prefix in applicable cases.
Validate descriptor receivers and built-in arguments
crates/vm/src/builtins/descriptor.rs, extra_tests/snippets/syntax_function_args.py
Method descriptor call paths require and validate positional self, and reject unsupported keywords. Tests cover receiver errors, keyword handling, and built-in argument behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: youknowone

Merge Risk: ⚪ Minimal · up to a0c02

No merge-blocking issue is established by the supplied evidence; normal checks can proceed.

Security Architecture Review

Security architecture risk: 🔵 Low · up to b7bf3

The checked call paths reject unsupported keywords before argument conversion or native execution while preserving receiver checks. However, the implementation applies signature-derived policy more broadly than the described audited annotations, and compatibility with external registrations remains unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The relevant attackable surface is Python calls to reachable native functions and method descriptors, including extension callables. The shared registration policy affects many generated callables, but the checked rejection paths do not grant additional native execution authority.

Security Findings and Attack Paths

  • observed — PassToBinder is not early validation of every keyword name. Binding can perform argument conversions before detecting leftover unsupported keywords. This is existing binder behavior, not an established PR-introduced vulnerability; binder success still precedes the ordinary bound native function body.

Trust Boundaries and Controls

  • observed — Caller-supplied keywords encounter registration-owned policy before conversion or native invocation on rejecting paths. Descriptor receiver checks remain ahead of keyword diagnostics, and empty keyword collections do not trigger rejection.

Resilience and Maintainability Implications

  • inferred — For calls rejected by keyword policy, the checked paths terminate before executing mutating native methods, so those methods have no partial state change to recover. Compatibility of custom or external registrations with inferred keyword metadata remains unverified.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.59% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 85 functions across 19 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting unsupported native keyword arguments before argument binding.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[ ] test: cpython/Lib/test/test_extcall.py (TODO: 7)

dependencies:

dependent tests: (no tests depend on extcall)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

Comment thread crates/vm/src/builtins/list.rs Outdated
)]
impl Py<PyList> {
#[pymethod]
#[pymethod(no_keywords)]

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.

Does no_keyword need to be an explicit marker? when a function take only positional arguments, it is automatically no_keyword. Does CPython distinguish explicit no keyword function and others?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the comment. I found no_keyword marker is not necessary. CPython uses ml_flags to distinguish keyword-capable and non-keyword function. CPython rejects keyword arguments with function with ml_flags with METH_O, METH_NOARGS, and METH_FASTCALL without METH_KEYWORDS. So, I agree no_keywords marker is redundant. We can drive the keyword dispatch policy from FromArgs::PARAMS.
I will patch it.

@youknowone youknowone left a comment

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.

Thank you! I left a few comments; though I didn't fully reviewed the design yet.

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 merge tests into single test not to sprawl out.
my recommendation is putting both of them into syntax_funcion_args.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, I’ll merge the tests into syntax_function_args.py.

Comment thread crates/derive-impl/src/pymodule.rs Outdated
}
}

struct FunctionItemMeta(ItemMetaInner);

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.

do you still need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FunctionItemMeta was introduced to support the no_keywords marker. Now that the marker is gone, it is equivalent to SimpleItemMeta, so I’ll remove it.

Comment thread crates/vm/src/function/method.rs Outdated
pub name: &'static str,
pub flags: PyMethodFlags,
pub doc: super::ItemDoc,
pub keyword_dispatch: KeywordDispatch,

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.

while all fields are public, it means keyword_dispatch is decidable regardless how its parameters looks like. I don't have a good picture how this is expected to be, but I don't agree to this design.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I introduced MethodDefSpec to simplify the function signature and accommodate future metadata changes, but it isn’t necessary here. Since such changes seem rare, the original new_method_def signature with an additional keyword_dispatch argument should be sufficient.

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.

MethodSpecDef itself might be useful, but at least keyword_dispatch must not be pub

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see. I choose not to make MethodSpecDef this time.

@widehyo1
widehyo1 force-pushed the fix/native-keyword-dispatch branch from b7bf32d to aa4904b Compare October 6, 2026 11:45
@codspeed

codspeed Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 62 untouched benchmarks
⏩ 4 skipped benchmarks1


Comparing widehyo1:fix/native-keyword-dispatch (3624faf) with main (1e72b22)

Open in CodSpeed

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@fanninpm

fanninpm commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@codspeedbot explain why performance improved

@codspeed

codspeed Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

@codspeedbot explain why performance improved

The +11% on loop_string.py is noise, not caused by this PR

The whole speedup comes from dictionary probing when the loop writes to the module globals. That cost depends on a random hash seed, not on the keyword-dispatch changes.

Where the time went

I compared the flame graphs of the base run (3e0e401) and the head run (aa4904b):

Function (inclusive time) Base Head Δ
Whole benchmark 936.6 µs 842.7 µs −93.9 µs
PyMapping::_ass_subscript (STORE_NAME) 419.3 µs 326.1 µs −93.2 µs
↳ Dict::insert 240.3 µs 147.0 µs −93.3 µs
  ↳ Dict::lookup 178.7 µs 85.3 µs −93.4 µs
execute_for_iter / PyStrIterator::next 174.8 / 74.3 µs 175.2 / 74.4 µs ≈0
PyDict::hash_or_unhashable 73.8 µs 73.8 µs 0

Nearly all of the 94 µs is in Dict::lookup. The string iterator, hashing, and the interpreter loop cost the same in both runs, to within 1 µs.

Why Dict::lookup changed

  • The benchmark is for char in string: pass. Each iteration stores char into the module globals dict, which goes through Dict::insert → Dict::lookup.
  • benches/microbenchmarks.rs builds Settings::default(), which sets hash_seed: None. Each benchmark process therefore picks a random string-hash secret.
  • That secret decides how many slots Dict::lookup probes before it finds char in the globals table. Some seeds find it right away and some need extra probes, so the benchmark tends to land on one of two levels, about 843 µs or about 937 µs.
  • main shows the same pattern with no related code changes. Dependency-only bumps (build(deps): bump which from 8.0.5 to 8.0.6 #8978, build(deps): bump bitflags from 2.13.1 to 2.13.2 #8979) moved loop_string.py by about ±11% in the same function. This PR's head simply drew the faster seed.

Why the PR can't cause it

The PR changes the native call path: builtin_func.rs, descriptor.rs, function/signature.rs, and the derive macros. This loop never calls a native function or method descriptor, and none of the changed code appears in either flame graph. Nothing in dict_inner.rs, str.rs, or frame.rs was touched.

Bottom line: the PR has no performance effect on loop_string.py; the improvement can be ignored. To stop these flips, you could set settings.hash_seed = Some(0) in bench_rustpython_code so every run hashes strings the same way. I haven't made that change, since it is outside this PR's scope.

Summary
  • Warming up the performance wizard...
  • Investigate the loop_string.py improvement — The whole delta is in the globals-dict lookup, which the hash seed controls. It is not caused by this PR.
  • Reply with explanation — Reply with explanation
  • Handling comment — Comment handling finished successfully

Learnings updated. See what CodSpeed remembers about this repository.

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

@youknowone
youknowone force-pushed the fix/native-keyword-dispatch branch from aa4904b to 3624faf Compare October 8, 2026 02:59
@youknowone
youknowone force-pushed the fix/native-keyword-dispatch branch from 3624faf to a0c0223 Compare October 8, 2026 09:02
widehyo1 and others added 4 commits October 9, 2026 00:20
Add explicit keyword dispatch metadata independently of inferred Rust
calling flags. Reject nonempty keywords before binding and conversion,
while preserving descriptor self checks and keyword-capable calls.

Annotate 31 builtins and list.append/list.clear, derive C-API policies
from ml_flags, and group dynamic registration metadata in MethodDefSpec.
Add regression coverage and remove the id doctest expected-failure marker.

Validation: workspace and C-API tests/clippy, VM without doc, keyword
regressions, and test_call/test_extcall/test_builtin/test_list passed.
The full snippet suite had 475 passes and 7 failures: six CPython-side
expectation mismatches and one missing optional SQLite module.

Assisted-by: Codex:GPT-6
Replace no_keywords annotations with registration-time inference from
FromArgs::PARAMS, including nested parameter metadata. Forward arguments
with keyword-capable parameters to the binder, preserving min/max body
validation and descriptor self checks.

Add regressions for raw arguments, keyword argument structs, and list.sort.

Tested: C-API tests, workspace and C-API Clippy, VM without doc, both keyword
regression snippets, and test_call/test_extcall/test_builtin/test_list.
Workspace tests reported eight _opcode snapshot failures. The full snippet
suite reported 477 passes and seven failures: six CPython-side failures
and one RustPython failure due to the missing optional SQLite module.

Assisted-by: Codex:GPT-6
Reuse SimpleItemMeta for pyfunction attributes and remove FunctionItemMeta.
Remove MethodDefSpec and restore the original new_method_def arguments
with an additional keyword_dispatch parameter, updating its callers.

Consolidate keyword dispatch regressions in syntax_function_args.py,
preserving their assertions and placing them before builtin shadowing.

Validation: workspace and C-API tests/clippy, VM without doc, the combined
snippet, and test_extcall/test_builtin/test_list/test_call passed.
The full snippet suite reported 477 passes and seven existing failures:
six CPython-side failures and one missing optional SQLite module.

Assisted-by: Codex:GPT-6
Remove `KeywordDispatch`, the `keyword_dispatch` field of `PyMethodDef`,
`PyMethodDef::with_keyword_dispatch`, `function::keyword_dispatch` and
the derive macros' `keyword_dispatch_tokens`. Native functions and method
descriptors reject nonempty keywords when `flags` lacks `KEYWORDS`, and
report the unqualified name when `flags` contains `VARARGS`.

`Context::new_method_def` takes `(name, f, flags, doc)` again. Add a
snippet for `int.__new__` called with a keyword argument.

Assisted-by: Claude Code:claude-opus-5-5
@youknowone
youknowone force-pushed the fix/native-keyword-dispatch branch from a0c0223 to d8469d3 Compare October 8, 2026 15:58
@youknowone

Copy link
Copy Markdown
Member

@widehyo1 please take a look. based on #9005

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants