Skip to content

Commit 2f96f8c

Browse files
committed
Use pathlib in cuda.core build hooks, tests and examples
Part of #2410. Replaces os.path with pathlib.Path in cuda_core/build_hooks.py, tests/helpers, the example test driver and the two examples that assemble CUDA include paths. ProgramOptions now accepts os.PathLike for every path-valued option (include_path, pre_include, create_pch, use_pch, pch_dir, fdevice_time_trace) and normalizes what it is given to pathlib.Path, so callers no longer have to convert back to str. Non-path values (False, range(...), ...) are left untouched, preserving the existing "silently ignored at compile time" behavior. name stays str (NVRTC uses it as a label and the program cache inspects it for a directory component); time stays str-or-bool because the same field is forwarded to LinkerOptions.time, which is a flag.
1 parent 3bd069a commit 2f96f8c

11 files changed

Lines changed: 180 additions & 68 deletions

File tree

cuda_core/build_hooks.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def _determine_cuda_major_version() -> str:
9898

9999
# Derive from the CUDA headers (the authoritative source for what we compile against).
100100
cuda_path = _get_cuda_path()
101-
cuda_h = os.path.join(cuda_path, "include", "cuda.h")
101+
cuda_h = Path(cuda_path, "include", "cuda.h")
102102
try:
103103
with open(cuda_h, encoding="utf-8") as f:
104104
for line in f:
@@ -162,10 +162,11 @@ def _build_cuda_core(debug=False):
162162
# It seems setuptools' wildcard support has problems for namespace packages,
163163
# so we explicitly spell out all Extension instances.
164164
def module_names():
165-
root_path = os.path.sep.join(["cuda", "core", ""])
166-
for filename in glob.glob(f"{root_path}/**/*.pyx", recursive=True):
167-
mod = filename[len(root_path) : -4]
168-
if sys.platform == "win32" and mod.replace(os.path.sep, "/") in _posix_only_modules:
165+
root_path = Path("cuda", "core")
166+
for filename in glob.glob(str(root_path / "**" / "*.pyx"), recursive=True):
167+
# Module names are always spelled POSIX-style, on every platform.
168+
mod = Path(filename).relative_to(root_path).with_suffix("").as_posix()
169+
if sys.platform == "win32" and mod in _posix_only_modules:
169170
continue
170171
yield mod
171172

@@ -176,12 +177,12 @@ def get_sources(mod_name):
176177
# Add module-specific .cpp file from _cpp/ directory if it exists
177178
# Example: _resource_handles.pyx finds _cpp/resource_handles.cpp.
178179
cpp_file = f"cuda/core/_cpp/{mod_name.lstrip('_')}.cpp"
179-
if os.path.exists(cpp_file):
180+
if Path(cpp_file).exists():
180181
sources.append(cpp_file)
181182

182183
return sources
183184

184-
all_include_dirs = [os.path.join(cuda_path, "include")]
185+
all_include_dirs = [str(Path(cuda_path, "include"))]
185186
extra_compile_args = []
186187
extra_link_args = []
187188
extra_cythonize_kwargs = {}
@@ -205,7 +206,7 @@ def get_sources(mod_name):
205206

206207
ext_modules = tuple(
207208
Extension(
208-
f"cuda.core.{mod.replace(os.path.sep, '.')}",
209+
f"cuda.core.{mod.replace('/', '.')}",
209210
sources=get_sources(mod),
210211
include_dirs=[
211212
"cuda/core/_include",
@@ -239,7 +240,7 @@ def get_sources(mod_name):
239240
return
240241

241242

242-
def _add_cython_include_paths_to_pth(wheel_path: str) -> None:
243+
def _add_cython_include_paths_to_pth(wheel_path: Path) -> None:
243244
"""
244245
Modify the .pth file in an editable install wheel to add Cython include paths.
245246
@@ -270,7 +271,7 @@ def _add_cython_include_paths_to_pth(wheel_path: str) -> None:
270271
# Create a temporary directory for wheel manipulation
271272
with tempfile.TemporaryDirectory() as tmpdir:
272273
tmpdir_path = Path(tmpdir)
273-
wheel_file = Path(wheel_path)
274+
wheel_file = wheel_path
274275

275276
# Extract the wheel
276277
extract_dir = tmpdir_path / "extracted"
@@ -325,7 +326,7 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non
325326
wheel_name = _build_meta.build_editable(wheel_directory, config_settings, metadata_directory)
326327

327328
# Patch the .pth file to add Cython include paths
328-
wheel_path = os.path.join(wheel_directory, wheel_name)
329+
wheel_path = Path(wheel_directory, wheel_name)
329330
_add_cython_include_paths_to_pth(wheel_path)
330331

331332
return wheel_name

cuda_core/cuda/core/_program.pyi

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This module provides :class:`Program` for compiling source code into
88
from __future__ import annotations
99

1010
from dataclasses import dataclass
11+
from os import PathLike
1112

1213
from cuda.bindings import nvrtc
1314
from cuda.core._linker import LinkerHandleT
@@ -141,6 +142,12 @@ class Program:
141142
class ProgramOptions:
142143
"""Customizable options for configuring :class:`Program`.
143144
145+
Every path-valued option (``include_path``, ``pre_include``, ``create_pch``,
146+
``use_pch``, ``pch_dir``, ``fdevice_time_trace``) accepts either a :class:`str`
147+
or any :class:`os.PathLike`, and stores it as a :class:`pathlib.Path`. Callers
148+
building paths with :mod:`pathlib` therefore never need to convert back to
149+
``str``.
150+
144151
Attributes
145152
----------
146153
name : str, optional
@@ -207,11 +214,11 @@ class ProgramOptions:
207214
undefine_macro : Union[str, list[str]], optional
208215
Cancel any previous definition of a macro, or list of macros.
209216
Default: None
210-
include_path : Union[str, list[str]], optional
217+
include_path : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
211218
Add the directory or directories to the list of directories to be searched for headers.
212219
Default: None
213-
pre_include : Union[str, list[str]], optional
214-
Preinclude one or more headers during preprocessing. Can be either a string or a list of strings.
220+
pre_include : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
221+
Preinclude one or more headers during preprocessing. Can be either a single path or a list of paths.
215222
Default: None
216223
no_source_include : bool, optional
217224
Disable the default behavior of adding the directory of each input source to the include path.
@@ -270,7 +277,7 @@ class ProgramOptions:
270277
no_cache : bool, optional
271278
Disable compiler caching.
272279
Default: False
273-
fdevice_time_trace : str, optional
280+
fdevice_time_trace : Union[str, os.PathLike], optional
274281
Generate time trace JSON for profiling compilation (NVRTC only).
275282
Default: None
276283
device_float128 : bool, optional
@@ -285,13 +292,13 @@ class ProgramOptions:
285292
pch : bool, optional
286293
Use default precompiled header (NVRTC only, CUDA 12.8+).
287294
Default: False
288-
create_pch : str, optional
295+
create_pch : Union[str, os.PathLike], optional
289296
Create precompiled header file (NVRTC only, CUDA 12.8+).
290297
Default: None
291-
use_pch : str, optional
298+
use_pch : Union[str, os.PathLike], optional
292299
Use specific precompiled header file (NVRTC only, CUDA 12.8+).
293300
Default: None
294-
pch_dir : str, optional
301+
pch_dir : Union[str, os.PathLike], optional
295302
PCH directory location (NVRTC only, CUDA 12.8+).
296303
Default: None
297304
pch_verbose : bool, optional
@@ -333,8 +340,8 @@ class ProgramOptions:
333340
gen_opt_lto: bool | None = None
334341
define_macro: str | tuple[str, str] | list[str | tuple[str, str]] | tuple[str | tuple[str, str], ...] | None = None
335342
undefine_macro: str | list[str] | tuple[str] | None = None
336-
include_path: str | list[str] | tuple[str] | None = None
337-
pre_include: str | list[str] | tuple[str] | None = None
343+
include_path: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
344+
pre_include: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
338345
no_source_include: bool | None = None
339346
std: str | None = None
340347
builtin_move_forward: bool | None = None
@@ -354,14 +361,14 @@ class ProgramOptions:
354361
fdevice_syntax_only: bool | None = None
355362
minimal: bool | None = None
356363
no_cache: bool | None = None
357-
fdevice_time_trace: str | None = None
364+
fdevice_time_trace: str | PathLike[str] | None = None
358365
device_float128: bool | None = None
359366
frandom_seed: str | None = None
360367
ofast_compile: str | None = None
361368
pch: bool | None = None
362-
create_pch: str | None = None
363-
use_pch: str | None = None
364-
pch_dir: str | None = None
369+
create_pch: str | PathLike[str] | None = None
370+
use_pch: str | PathLike[str] | None = None
371+
pch_dir: str | PathLike[str] | None = None
365372
pch_verbose: bool | None = None
366373
pch_messages: bool | None = None
367374
instantiate_templates_in_pch: bool | None = None
@@ -418,12 +425,23 @@ class ProgramOptions:
418425
"""Convert extra_sources to bytes format for NVVM."""
419426
__all__ = ['Program', 'ProgramOptions']
420427
ProgramHandleT = nvrtc.nvrtcProgram | int | LinkerHandleT
428+
_PATH_OPTION_FIELDS = ('include_path', 'pre_include', 'create_pch', 'use_pch', 'pch_dir', 'fdevice_time_trace')
421429
_nvvm_module = None
422430
_nvvm_import_attempted = False
423431

424432
def _can_load_generated_ptx() -> bool:
425433
"""Check if the driver can load PTX generated by the current NVRTC version."""
426434

435+
def _coerce_path_option(value):
436+
"""Normalize a path-valued :class:`ProgramOptions` field.
437+
438+
``str`` / :class:`os.PathLike` becomes :class:`pathlib.Path`; a ``list``
439+
or ``tuple`` has its ``str`` / :class:`os.PathLike` items converted while
440+
keeping the container type. Anything else is returned unchanged, so the
441+
"silently ignored at compile time" behavior of non-path values (``False``,
442+
``range(...)``, ...) is unaffected.
443+
"""
444+
427445
def _program_compile_uncached(program, target_type, name_expressions, logs):
428446
"""Run ``Program_compile`` without the cache wrapper.
429447

cuda_core/cuda/core/_program.pyx

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This module provides :class:`Program` for compiling source code into
1010
from __future__ import annotations
1111

1212
from dataclasses import dataclass
13+
from os import PathLike
14+
from pathlib import Path
1315
import threading
1416
from typing import TYPE_CHECKING
1517
from warnings import warn
@@ -294,6 +296,12 @@ cdef class Program:
294296
class ProgramOptions:
295297
"""Customizable options for configuring :class:`Program`.
296298

299+
Every path-valued option (``include_path``, ``pre_include``, ``create_pch``,
300+
``use_pch``, ``pch_dir``, ``fdevice_time_trace``) accepts either a :class:`str`
301+
or any :class:`os.PathLike`, and stores it as a :class:`pathlib.Path`. Callers
302+
building paths with :mod:`pathlib` therefore never need to convert back to
303+
``str``.
304+
297305
Attributes
298306
----------
299307
name : str, optional
@@ -360,11 +368,11 @@ class ProgramOptions:
360368
undefine_macro : Union[str, list[str]], optional
361369
Cancel any previous definition of a macro, or list of macros.
362370
Default: None
363-
include_path : Union[str, list[str]], optional
371+
include_path : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
364372
Add the directory or directories to the list of directories to be searched for headers.
365373
Default: None
366-
pre_include : Union[str, list[str]], optional
367-
Preinclude one or more headers during preprocessing. Can be either a string or a list of strings.
374+
pre_include : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
375+
Preinclude one or more headers during preprocessing. Can be either a single path or a list of paths.
368376
Default: None
369377
no_source_include : bool, optional
370378
Disable the default behavior of adding the directory of each input source to the include path.
@@ -423,7 +431,7 @@ class ProgramOptions:
423431
no_cache : bool, optional
424432
Disable compiler caching.
425433
Default: False
426-
fdevice_time_trace : str, optional
434+
fdevice_time_trace : Union[str, os.PathLike], optional
427435
Generate time trace JSON for profiling compilation (NVRTC only).
428436
Default: None
429437
device_float128 : bool, optional
@@ -438,13 +446,13 @@ class ProgramOptions:
438446
pch : bool, optional
439447
Use default precompiled header (NVRTC only, CUDA 12.8+).
440448
Default: False
441-
create_pch : str, optional
449+
create_pch : Union[str, os.PathLike], optional
442450
Create precompiled header file (NVRTC only, CUDA 12.8+).
443451
Default: None
444-
use_pch : str, optional
452+
use_pch : Union[str, os.PathLike], optional
445453
Use specific precompiled header file (NVRTC only, CUDA 12.8+).
446454
Default: None
447-
pch_dir : str, optional
455+
pch_dir : Union[str, os.PathLike], optional
448456
PCH directory location (NVRTC only, CUDA 12.8+).
449457
Default: None
450458
pch_verbose : bool, optional
@@ -487,8 +495,8 @@ class ProgramOptions:
487495
gen_opt_lto: bool | None = None
488496
define_macro: str | tuple[str, str] | list[str | tuple[str, str]] | tuple[str | tuple[str, str], ...] | None = None
489497
undefine_macro: str | list[str] | tuple[str] | None = None
490-
include_path: str | list[str] | tuple[str] | None = None
491-
pre_include: str | list[str] | tuple[str] | None = None
498+
include_path: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
499+
pre_include: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
492500
no_source_include: bool | None = None
493501
std: str | None = None
494502
builtin_move_forward: bool | None = None
@@ -508,14 +516,14 @@ class ProgramOptions:
508516
fdevice_syntax_only: bool | None = None
509517
minimal: bool | None = None
510518
no_cache: bool | None = None
511-
fdevice_time_trace: str | None = None
519+
fdevice_time_trace: str | PathLike[str] | None = None
512520
device_float128: bool | None = None
513521
frandom_seed: str | None = None
514522
ofast_compile: str | None = None
515523
pch: bool | None = None
516-
create_pch: str | None = None
517-
use_pch: str | None = None
518-
pch_dir: str | None = None
524+
create_pch: str | PathLike[str] | None = None
525+
use_pch: str | PathLike[str] | None = None
526+
pch_dir: str | PathLike[str] | None = None
519527
pch_verbose: bool | None = None
520528
pch_messages: bool | None = None
521529
instantiate_templates_in_pch: bool | None = None
@@ -528,6 +536,12 @@ class ProgramOptions:
528536
if self.name is None:
529537
self.name = "default_program"
530538
self._name = self.name.encode()
539+
# Path-valued options accept str or os.PathLike; normalize to Path so
540+
# callers never have to convert back to str just to build options.
541+
for _field in _PATH_OPTION_FIELDS:
542+
_value = getattr(self, _field)
543+
if _value is not None:
544+
setattr(self, _field, _coerce_path_option(_value))
531545
# Set arch to default if not provided
532546
if self.arch is None:
533547
self.arch = f"sm_{Device().arch}"
@@ -632,6 +646,42 @@ class ProgramOptions:
632646
# =============================================================================
633647

634648

649+
# ``ProgramOptions`` fields that name a filesystem path. ``str`` and
650+
# ``os.PathLike`` values for these are normalized to :class:`pathlib.Path` in
651+
# ``ProgramOptions.__post_init__`` so everything downstream sees one type.
652+
#
653+
# ``name`` is not here: NVRTC uses it as the source *filename*, but it is a
654+
# plain label (``ProgramOptions.__post_init__`` encodes it, and the program
655+
# cache inspects it for a directory component), so it stays ``str``. ``time``
656+
# is not here either: NVRTC treats it as an output filename, but the same
657+
# field is forwarded to ``LinkerOptions.time`` (a bool flag) for PTX inputs.
658+
_PATH_OPTION_FIELDS = (
659+
"include_path",
660+
"pre_include",
661+
"create_pch",
662+
"use_pch",
663+
"pch_dir",
664+
"fdevice_time_trace",
665+
)
666+
667+
668+
def _coerce_path_option(value):
669+
"""Normalize a path-valued :class:`ProgramOptions` field.
670+
671+
``str`` / :class:`os.PathLike` becomes :class:`pathlib.Path`; a ``list``
672+
or ``tuple`` has its ``str`` / :class:`os.PathLike` items converted while
673+
keeping the container type. Anything else is returned unchanged, so the
674+
"silently ignored at compile time" behavior of non-path values (``False``,
675+
``range(...)``, ...) is unaffected.
676+
"""
677+
if isinstance(value, (str, PathLike)):
678+
return Path(value)
679+
if isinstance(value, (list, tuple)):
680+
coerced = [Path(v) if isinstance(v, (str, PathLike)) else v for v in value]
681+
return tuple(coerced) if isinstance(value, tuple) else coerced
682+
return value
683+
684+
635685
def _program_compile_uncached(program, target_type, name_expressions, logs):
636686
"""Run ``Program_compile`` without the cache wrapper.
637687
@@ -1129,13 +1179,15 @@ cdef inline list _prepare_nvrtc_options_impl(object opts):
11291179
for macro in opts.undefine_macro:
11301180
options.append(f"--undefine-macro={macro}")
11311181
if opts.include_path is not None:
1132-
if isinstance(opts.include_path, str):
1182+
# ``__post_init__`` turns str/PathLike into Path, but the dataclass is
1183+
# mutable, so accept either form here.
1184+
if isinstance(opts.include_path, (str, PathLike)):
11331185
options.append(f"--include-path={opts.include_path}")
11341186
elif is_sequence(opts.include_path):
11351187
for path in opts.include_path:
11361188
options.append(f"--include-path={path}")
11371189
if opts.pre_include is not None:
1138-
if isinstance(opts.pre_include, str):
1190+
if isinstance(opts.pre_include, (str, PathLike)):
11391191
options.append(f"--pre-include={opts.pre_include}")
11401192
elif is_sequence(opts.pre_include):
11411193
for header in opts.pre_include:

cuda_core/cuda/core/utils/_program_cache/_keys.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import abc
1515
import collections.abc
1616
import hashlib
17+
import os
1718
from typing import Any, Callable, Sequence
1819

1920
# Mutual-dependency contract: this module imports ProgramOptions from
@@ -269,7 +270,8 @@ def _option_is_set(options: ProgramOptions, name: str) -> bool:
269270
270271
- Boolean flags (``pch``): truthy only.
271272
- str-or-sequence fields (``include_path``, ``pre_include``): ``str``
272-
(including empty) or a non-empty ``collections.abc.Sequence`` (list,
273+
(including empty), ``os.PathLike`` (``ProgramOptions`` normalizes both
274+
to ``pathlib.Path``), or a non-empty ``collections.abc.Sequence`` (list,
273275
tuple, range, user subclass, ...); everything else (``False``, ``int``,
274276
empty sequence, ``None``) is ignored by the compiler and must not
275277
trigger a cache-time guard.
@@ -284,11 +286,12 @@ def _option_is_set(options: ProgramOptions, name: str) -> bool:
284286
if name in _BOOLEAN_OPTION_FIELDS:
285287
return bool(value)
286288
if name in _STR_OR_SEQUENCE_OPTION_FIELDS:
287-
# Mirror ``_prepare_nvrtc_options_impl``: it checks ``isinstance(v, str)``
288-
# first, then ``is_sequence(v)`` (which is ``isinstance(v, Sequence)``).
289-
# We therefore accept any ``collections.abc.Sequence`` (range, deque,
290-
# user subclass, etc.), not just list/tuple.
291-
if isinstance(value, str):
289+
# Mirror ``_prepare_nvrtc_options_impl``: it checks
290+
# ``isinstance(v, (str, os.PathLike))`` first, then ``is_sequence(v)``
291+
# (which is ``isinstance(v, Sequence)``). We therefore accept any
292+
# ``collections.abc.Sequence`` (range, deque, user subclass, etc.),
293+
# not just list/tuple.
294+
if isinstance(value, (str, os.PathLike)):
292295
return True
293296
if isinstance(value, collections.abc.Sequence):
294297
return len(value) > 0

0 commit comments

Comments
 (0)