Skip to content

Commit c2dab7f

Browse files
committed
Address cuda-core property review feedback
1 parent 20e6f06 commit c2dab7f

2 files changed

Lines changed: 64 additions & 122 deletions

File tree

cuda_core/cuda/core/_utils/properties.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,4 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
6-
def python_property(fget=None, fset=None, fdel=None, doc=None):
7-
"""Create a Python property without Cython's cdef-class @property lowering."""
8-
return property(fget, fset, fdel, doc)
5+
python_property = property

cuda_core/tests/test_cython_property_descriptors.py

Lines changed: 63 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -6,132 +6,77 @@
66

77
import importlib
88
import inspect
9-
import re
9+
import pkgutil
1010
import types
11-
from dataclasses import dataclass
12-
from pathlib import Path
1311

1412
import pytest
1513

16-
from cuda.core.system import CUDA_BINDINGS_NVML_IS_COMPATIBLE
14+
import cuda.core
15+
import cuda.core.graph
16+
import cuda.core.system
1717

1818
pytestmark = pytest.mark.no_cuda
1919

20-
_CORE_ROOT = Path(__file__).resolve().parents[1] / "cuda" / "core"
21-
_CDEF_CLASS_RE = re.compile(r"^cdef\s+class\s+([A-Za-z_]\w*)\b")
22-
_DEF_RE = re.compile(r"def\s+([A-Za-z_]\w*)\s*\(")
23-
_PROPERTY_ASSIGN_RE = re.compile(r"([A-Za-z_]\w*)\s*=\s*python_property\(")
24-
25-
26-
@dataclass(frozen=True)
27-
class CythonProperty:
28-
module: str
29-
source: Path
30-
line: int
31-
class_name: str
32-
name: str
33-
decorator: str
34-
35-
36-
def _module_for_source(source: Path) -> str:
37-
relative = source.relative_to(_CORE_ROOT)
38-
if source.suffix == ".pyx":
39-
return "cuda.core." + ".".join(relative.with_suffix("").parts)
40-
if relative.parts[0] == "system":
41-
return "cuda.core.system._device"
42-
raise ValueError(f"No module mapping for {source}")
43-
44-
45-
def _iter_cython_properties():
46-
for source in sorted((*_CORE_ROOT.rglob("*.pyx"), *_CORE_ROOT.rglob("*.pxi"))):
47-
module = _module_for_source(source)
48-
class_name = None
49-
pending_decorator = None
50-
pending_line = None
51-
for line_number, line in enumerate(source.read_text(encoding="utf-8").splitlines(), start=1):
52-
class_match = _CDEF_CLASS_RE.match(line)
53-
if class_match is not None:
54-
class_name = class_match.group(1)
55-
pending_decorator = None
56-
pending_line = None
57-
continue
58-
59-
if class_name is not None and line and not line[0].isspace() and line.strip() and not line.startswith("#"):
60-
class_name = None
61-
pending_decorator = None
62-
pending_line = None
6320

64-
if class_name is None:
21+
_OPTIONAL_IMPORT_FAILURES = {"cuda.core._tensor_bridge"}
22+
23+
_GETSET_FIELD_ALLOWLIST = {
24+
("cuda.core._kernel_arg_handler", "ParamHolder", "ptr"),
25+
("cuda.core._layout", "_StridedLayout", "itemsize"),
26+
("cuda.core._layout", "_StridedLayout", "slice_offset"),
27+
("cuda.core._memoryview", "StridedMemoryView", "device_id"),
28+
("cuda.core._memoryview", "StridedMemoryView", "exporting_obj"),
29+
("cuda.core._memoryview", "StridedMemoryView", "is_device_accessible"),
30+
("cuda.core._memoryview", "StridedMemoryView", "ptr"),
31+
("cuda.core._memoryview", "StridedMemoryView", "readonly"),
32+
("cuda.core._memoryview", "_StridedMemoryViewProxy", "has_dlpack"),
33+
("cuda.core._memoryview", "_StridedMemoryViewProxy", "obj"),
34+
}
35+
36+
37+
def _iter_cuda_core_modules():
38+
roots = (cuda.core, cuda.core.graph, cuda.core.system)
39+
module_names = set()
40+
for root in roots:
41+
for info in pkgutil.walk_packages(root.__path__, root.__name__ + "."):
42+
module_names.add(info.name)
43+
44+
for module_name in sorted(module_names):
45+
try:
46+
yield importlib.import_module(module_name)
47+
except ImportError:
48+
if module_name in _OPTIONAL_IMPORT_FAILURES:
6549
continue
66-
67-
stripped = line.strip()
68-
if stripped in {"@property", "@python_property"}:
69-
pending_decorator = stripped
70-
pending_line = line_number
71-
continue
72-
73-
assignment_match = _PROPERTY_ASSIGN_RE.match(stripped)
74-
if assignment_match is not None:
75-
yield CythonProperty(
76-
module=module,
77-
source=source,
78-
line=line_number,
79-
class_name=class_name,
80-
name=assignment_match.group(1),
81-
decorator="python_property",
82-
)
83-
pending_decorator = None
84-
pending_line = None
85-
continue
86-
87-
if pending_decorator is not None:
88-
def_match = _DEF_RE.match(stripped)
89-
if def_match is not None:
90-
yield CythonProperty(
91-
module=module,
92-
source=source,
93-
line=pending_line,
94-
class_name=class_name,
95-
name=def_match.group(1),
96-
decorator=pending_decorator,
97-
)
98-
pending_decorator = None
99-
pending_line = None
100-
101-
102-
_CYTHON_PROPERTIES = tuple(_iter_cython_properties())
103-
104-
105-
def _property_id(cython_property: CythonProperty) -> str:
106-
return f"{cython_property.module}.{cython_property.class_name}.{cython_property.name}"
107-
108-
109-
def test_cython_cdef_class_getters_use_python_property_decorator():
110-
assert _CYTHON_PROPERTIES
111-
cython_properties = [
112-
f"{prop.source.relative_to(_CORE_ROOT)}:{prop.line}: {prop.class_name}.{prop.name}"
113-
for prop in _CYTHON_PROPERTIES
114-
if prop.decorator == "@property"
50+
raise
51+
52+
53+
def _iter_cuda_core_classes():
54+
for module in _iter_cuda_core_modules():
55+
for _, cls in inspect.getmembers(module, inspect.isclass):
56+
if cls.__module__ == module.__name__:
57+
yield cls
58+
59+
60+
def _is_allowed_getset_descriptor(cls, name, descriptor) -> bool:
61+
if name in {"__dict__", "__weakref__"}:
62+
return True
63+
# Typed cdef fields generate getset descriptors too, but their docs follow
64+
# this compact "field: type" form rather than a property docstring.
65+
doc = descriptor.__doc__
66+
if doc is not None and doc.startswith(f"{name}:"):
67+
return True
68+
return (cls.__module__, cls.__qualname__, name) in _GETSET_FIELD_ALLOWLIST
69+
70+
71+
def test_cuda_core_classes_do_not_expose_cython_property_getset_descriptors():
72+
classes = tuple(_iter_cuda_core_classes())
73+
unexpected_getsets = [
74+
f"{cls.__module__}.{cls.__qualname__}.{name}"
75+
for cls in classes
76+
for name, descriptor in vars(cls).items()
77+
if isinstance(descriptor, types.GetSetDescriptorType)
78+
and not _is_allowed_getset_descriptor(cls, name, descriptor)
11579
]
11680

117-
assert not cython_properties
118-
119-
120-
@pytest.mark.parametrize(
121-
"cython_property",
122-
[
123-
pytest.param(cython_property, id=_property_id(cython_property))
124-
for cython_property in _CYTHON_PROPERTIES
125-
if cython_property.decorator != "@property"
126-
],
127-
)
128-
def test_cython_properties_are_python_properties(cython_property: CythonProperty):
129-
if cython_property.module.startswith("cuda.core.system.") and not CUDA_BINDINGS_NVML_IS_COMPATIBLE:
130-
pytest.skip("cuda.core.system extension modules require NVML-compatible bindings")
131-
132-
module = importlib.import_module(cython_property.module)
133-
cls = getattr(module, cython_property.class_name)
134-
descriptor = inspect.getattr_static(cls, cython_property.name)
135-
136-
assert isinstance(descriptor, property)
137-
assert not isinstance(descriptor, types.GetSetDescriptorType)
81+
assert classes
82+
assert not unexpected_getsets

0 commit comments

Comments
 (0)