Skip to content

Commit 29f4e0f

Browse files
authored
Merge branch 'main' into revert-dependabot
2 parents b6f1701 + b92e272 commit 29f4e0f

19 files changed

Lines changed: 606 additions & 25 deletions

File tree

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ repos:
3232
language: python
3333
additional_dependencies:
3434
- https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl
35-
exclude: '(.*pixi\.lock)|(\.git_archival\.txt)|(.*\.patch$)'
35+
exclude: '(.*pixi\.lock)|(\.git_archival\.txt)|(.*\.patch$)|(^cuda_core/cuda/core/_vendored/)'
3636
args: ["--fix"]
3737

3838
- id: no-markdown-in-docs-source
@@ -111,6 +111,7 @@ repos:
111111
alias: mypy-cuda-core
112112
name: mypy-cuda-core
113113
files: ^cuda_core/cuda/.*\.(py|pyi)$
114+
exclude: ^cuda_core/cuda/core/_vendored/
114115
pass_filenames: false
115116
args: [--config-file=cuda_core/pyproject.toml, cuda_core/cuda/core]
116117
additional_dependencies:

AGENTS.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,44 @@ repos/{owner}/{repo}/milestones --jq '.[].title'`, and pick the best match.
113113
end on clarifications unless truly blocked. Every rollout should conclude
114114
with a concrete edit or an explicit blocker plus a targeted question.
115115

116+
## Test authorship markers
117+
118+
Use pytest markers to make the provenance of newly added unit tests explicit.
119+
Keep this provenance system minimal: choose from only these three markers.
120+
Place the marker immediately above each test function. A class-level marker is
121+
acceptable only when every test method in the class has the same provenance.
122+
Do not use module-level `pytestmark` for authorship provenance; it is too easy
123+
to miss in large files and makes later per-test provenance changes ambiguous.
124+
125+
- `@pytest.mark.agent_authored(model="<model>")`: the test was authored by an
126+
agent and has not yet been materially reviewed or rewritten by a human.
127+
Agents must add this marker when generating new unit tests, for example:
128+
129+
```python
130+
import pytest
131+
132+
@pytest.mark.agent_authored(model="gpt-5.5")
133+
def test_something():
134+
...
135+
```
136+
137+
- `@pytest.mark.human_reviewed`: a human has materially reviewed or rewritten
138+
an agent-authored test. Prefer replacing
139+
`@pytest.mark.agent_authored(model="<model>")` with this marker instead of
140+
keeping both.
141+
- `@pytest.mark.human_authored`: the test was authored by a human, or
142+
rewritten enough that the authorship is primarily human.
143+
144+
Use at most one authorship marker per test. Treat missing markers as legacy or
145+
unknown provenance, not as implicit `@pytest.mark.human_authored`. Because
146+
these are pytest markers, tests can be selected with `pytest -m`, for example
147+
`pytest -m agent_authored`.
148+
149+
When an agent notices a human adding a new test or materially modifying an
150+
existing test, suggest adding `@pytest.mark.human_authored` or replacing
151+
`@pytest.mark.agent_authored(model="<model>")` with
152+
`@pytest.mark.human_reviewed` as appropriate.
153+
116154

117155
# Editing constraints
118156

cuda_bindings/pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ required_plugins = "pytest-benchmark"
9292
addopts = "--benchmark-disable --showlocals"
9393
norecursedirs = ["tests/cython", "examples"]
9494
xfail_strict = true
95+
# Keep this authorship marker registry in sync across all pytest config roots.
96+
# Search for "agent_authored(model)" before editing.
97+
markers = [
98+
"agent_authored(model): agent-authored test not yet materially human-reviewed",
99+
"human_reviewed: agent-authored test materially reviewed or rewritten by a human",
100+
"human_authored: test authored primarily by a human",
101+
]
95102

96103
[tool.setuptools_scm]
97104
root = ".."

cuda_core/AGENTS.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,104 @@ so that they are documented but don't appear in the main index.
145145
### API stability
146146

147147
Reviews should point out where existing public APIs are broken.
148+
149+
### API lifecycle and deprecations
150+
151+
`cuda.core` follows SemVer (see `docs/source/support.rst`):
152+
153+
- **New APIs** may be added at any time (`x.Y.0`). They MUST have a
154+
`@versionadded` decorator, unless the docstring formatting requires it to be
155+
manually-specified.
156+
- **Breaking removals** only happen in **major releases** (`X.0.0`).
157+
- Per the support policy, a deprecation notice must be present for **at least
158+
one minor release** before the API is actually removed. The deprecation notice
159+
should use the `@deprecated` decorator, unless
160+
- Changes should be notated in the code and also in the release notes in the
161+
"Deprecated APIs" section.
162+
163+
**Annotating a new API** — Use the `versionadded` decorator from the vendored
164+
`cuda.core._vendored.deprecated.sphinx` module:
165+
166+
```python
167+
168+
from cuda.core._vendored.deprecated.sphinx import versionadded
169+
170+
@versionadded(version="1.2.0")
171+
def new_feature(...):
172+
"""Short description.
173+
"""
174+
```
175+
176+
Alternatively, if the vagaries of how we implement functions in Cython does not
177+
allow this, you can add the reST `versionadded` directive directly:
178+
179+
```python
180+
def new_feature(...):
181+
"""Short description.
182+
183+
.. versionadded:: 1.2.0
184+
"""
185+
```
186+
187+
**Annotating a changed API** — Use the `versionchanged` decorator from the
188+
vendored `cuda.core._vendored.deprecated.sphinx` module:
189+
190+
```python
191+
192+
from cuda.core._vendored.deprecated.sphinx import versionchanged
193+
194+
@versionchanged(version="1.2.0", reason="The old version was broken because...")
195+
def new_feature(...):
196+
"""Short description.
197+
"""
198+
```
199+
200+
Alternatively, if the vagaries of how we implement functions in Cython does not
201+
allow this, you can add the reST `versionchanged` directive directly:
202+
203+
```python
204+
def new_feature(...):
205+
"""Short description.
206+
207+
.. versionchanged:: 1.2.0
208+
The old version was broken because...
209+
"""
210+
```
211+
212+
**Deprecating an existing API** — use the `@deprecated` decorator from the
213+
vendored `cuda.core._vendored.deprecated.sphinx` module and add a
214+
`.. deprecated::` directive in the docstring. The decorator emits a
215+
`DeprecationWarning` at call time; the docstring directive surfaces it in the
216+
generated docs.
217+
218+
```python
219+
from cuda.core._vendored.deprecated.sphinx import deprecated
220+
221+
@deprecated(version="1.2.0", reason="Use `new_feature` instead.")`
222+
def old_feature(...):
223+
"""Short description.
224+
"""
225+
```
226+
227+
Rules to follow when deprecating:
228+
229+
- The `version=` argument must be the **first** in which the
230+
deprecation appears, not the release in which removal is planned.
231+
- The `reason=` string must name the replacement (if one exists) so users
232+
know what to migrate to.
233+
- Keep the old implementation fully functional — do not change its behavior,
234+
only add the decorator.
235+
- The deprecated API must remain in the codebase for **at least one full minor
236+
release cycle** before it can be removed in a subsequent major release.
237+
238+
**Removing a deprecated API** — removals land in a **major release**. Before
239+
removing, verify that the deprecation has been present since at least the
240+
previous minor release. Remove the decorator, the implementation, and any
241+
`__all__` entry; update `api.rst` and the release notes accordingly.
242+
243+
## Vendored code
244+
245+
The `cuda/core/_vendored` directory contains vendored code from third-party
246+
sources. It should not be modified, except to update the dependency or under
247+
exceptional circumstances, in which case any modifications should be clearly
248+
marked.

cuda_core/cuda/core/_vendored/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Vendored from the Deprecated package (https://pypi.org/project/Deprecated/),
2+
# version 1.3.1, (c) Laurent LAPORTE, MIT License.
3+
# Modified to remove the dependency on the `wrapt` package.
4+
5+
from cuda.core._vendored.deprecated.classic import deprecated
6+
from cuda.core._vendored.deprecated.params import deprecated_params
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Vendored from the Deprecated package (https://pypi.org/project/Deprecated/),
2+
# version 1.3.1, (c) Laurent LAPORTE, MIT License.
3+
# Modified to remove the dependency on the `wrapt` package.
4+
5+
import functools
6+
import inspect
7+
import warnings
8+
9+
# stacklevel=2 points past the wrapper to the actual call site
10+
_routine_stacklevel = 2
11+
_class_stacklevel = 2
12+
13+
string_types = (bytes, str)
14+
15+
16+
class ClassicAdapter:
17+
"""
18+
Classic adapter -- *for advanced usage only*
19+
20+
This adapter is used to get the deprecation message according to the wrapped
21+
object type: class, function, standard method, static method, or class method.
22+
23+
This is the base class of the :class:`~deprecated.sphinx.SphinxAdapter` class
24+
which is used to update the wrapped object docstring.
25+
"""
26+
27+
def __init__(self, reason="", version="", action=None, category=DeprecationWarning, extra_stacklevel=0):
28+
self.reason = reason or ""
29+
self.version = version or ""
30+
self.action = action
31+
self.category = category
32+
self.extra_stacklevel = extra_stacklevel
33+
34+
def get_deprecated_msg(self, wrapped, instance):
35+
if instance is None:
36+
if inspect.isclass(wrapped):
37+
fmt = "Call to deprecated class {name}."
38+
else:
39+
fmt = "Call to deprecated function (or staticmethod) {name}."
40+
else:
41+
if inspect.isclass(instance):
42+
fmt = "Call to deprecated class method {name}."
43+
else:
44+
fmt = "Call to deprecated method {name}."
45+
if self.reason:
46+
fmt += " ({reason})"
47+
if self.version:
48+
fmt += " -- Deprecated since version {version}."
49+
return fmt.format(name=wrapped.__name__, reason=self.reason or "", version=self.version or "")
50+
51+
def __call__(self, wrapped):
52+
if inspect.isclass(wrapped):
53+
old_new1 = wrapped.__new__
54+
55+
def wrapped_cls(cls, *args, **kwargs):
56+
msg = self.get_deprecated_msg(wrapped, None)
57+
stacklevel = _class_stacklevel + self.extra_stacklevel
58+
if self.action:
59+
with warnings.catch_warnings():
60+
warnings.simplefilter(self.action, self.category)
61+
warnings.warn(msg, category=self.category, stacklevel=stacklevel)
62+
else:
63+
warnings.warn(msg, category=self.category, stacklevel=stacklevel)
64+
if old_new1 is object.__new__:
65+
return old_new1(cls)
66+
return old_new1(cls, *args, **kwargs)
67+
68+
wrapped.__new__ = staticmethod(wrapped_cls)
69+
return wrapped
70+
71+
elif inspect.isroutine(wrapped):
72+
adapter = self
73+
74+
@functools.wraps(wrapped)
75+
def wrapper(*args, **kwargs):
76+
msg = adapter.get_deprecated_msg(wrapped, None)
77+
stacklevel = _routine_stacklevel + adapter.extra_stacklevel
78+
if adapter.action:
79+
with warnings.catch_warnings():
80+
warnings.simplefilter(adapter.action, adapter.category)
81+
warnings.warn(msg, category=adapter.category, stacklevel=stacklevel)
82+
else:
83+
warnings.warn(msg, category=adapter.category, stacklevel=stacklevel)
84+
return wrapped(*args, **kwargs)
85+
86+
return wrapper
87+
88+
else:
89+
raise TypeError(repr(type(wrapped)))
90+
91+
92+
def deprecated(*args, **kwargs):
93+
"""
94+
Decorator which can be used to mark functions as deprecated.
95+
96+
It will result in a warning being emitted when the function is used.
97+
"""
98+
if args and isinstance(args[0], string_types):
99+
kwargs["reason"] = args[0]
100+
args = args[1:]
101+
102+
if args and not callable(args[0]):
103+
raise TypeError(repr(type(args[0])))
104+
105+
if args:
106+
adapter_cls = kwargs.pop("adapter_cls", ClassicAdapter)
107+
adapter = adapter_cls(**kwargs)
108+
wrapped = args[0]
109+
return adapter(wrapped)
110+
111+
return functools.partial(deprecated, **kwargs)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Vendored from the Deprecated package (https://pypi.org/project/Deprecated/),
2+
# version 1.3.1, (c) Laurent LAPORTE, MIT License.
3+
# Modified to remove the dependency on the `wrapt` package.
4+
5+
import collections
6+
import functools
7+
import inspect
8+
import warnings
9+
10+
11+
class DeprecatedParams:
12+
"""
13+
Decorator for functions where one or more parameters are deprecated.
14+
"""
15+
16+
def __init__(self, param, reason="", category=DeprecationWarning):
17+
self.messages = {}
18+
self.category = category
19+
self.populate_messages(param, reason=reason)
20+
21+
def populate_messages(self, param, reason=""):
22+
if isinstance(param, dict):
23+
self.messages.update(param)
24+
elif isinstance(param, str):
25+
fmt = "'{param}' parameter is deprecated"
26+
reason = reason or fmt.format(param=param)
27+
self.messages[param] = reason
28+
else:
29+
raise TypeError(param)
30+
31+
def check_params(self, signature, *args, **kwargs):
32+
binding = signature.bind(*args, **kwargs)
33+
bound = collections.OrderedDict(binding.arguments, **binding.kwargs)
34+
return [param for param in bound if param in self.messages]
35+
36+
def warn_messages(self, messages):
37+
for message in messages:
38+
warnings.warn(message, category=self.category, stacklevel=3)
39+
40+
def __call__(self, f):
41+
signature = inspect.signature(f)
42+
43+
@functools.wraps(f)
44+
def wrapper(*args, **kwargs):
45+
invalid_params = self.check_params(signature, *args, **kwargs)
46+
self.warn_messages([self.messages[param] for param in invalid_params])
47+
return f(*args, **kwargs)
48+
49+
return wrapper
50+
51+
52+
deprecated_params = DeprecatedParams

0 commit comments

Comments
 (0)