From 2f96f8c4191a08090bc53659b3358e3589530928 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Tue, 4 Aug 2026 15:28:09 -0700 Subject: [PATCH] Use pathlib in cuda.core build hooks, tests and examples Part of NVIDIA#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. --- cuda_core/build_hooks.py | 23 +++--- cuda_core/cuda/core/_program.pyi | 44 +++++++--- cuda_core/cuda/core/_program.pyx | 82 +++++++++++++++---- .../cuda/core/utils/_program_cache/_keys.py | 15 ++-- cuda_core/docs/source/release/1.2.0-notes.rst | 8 ++ cuda_core/examples/thread_block_cluster.py | 10 +-- cuda_core/examples/tma_tensor_map.py | 10 +-- .../example_tests/test_basic_examples.py | 10 +-- cuda_core/tests/helpers/__init__.py | 11 +-- cuda_core/tests/test_program.py | 28 +++++++ cuda_core/tests/test_program_cache.py | 7 +- 11 files changed, 180 insertions(+), 68 deletions(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 626d50355ab..aec1478c930 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -98,7 +98,7 @@ def _determine_cuda_major_version() -> str: # Derive from the CUDA headers (the authoritative source for what we compile against). cuda_path = _get_cuda_path() - cuda_h = os.path.join(cuda_path, "include", "cuda.h") + cuda_h = Path(cuda_path, "include", "cuda.h") try: with open(cuda_h, encoding="utf-8") as f: for line in f: @@ -162,10 +162,11 @@ def _build_cuda_core(debug=False): # It seems setuptools' wildcard support has problems for namespace packages, # so we explicitly spell out all Extension instances. def module_names(): - root_path = os.path.sep.join(["cuda", "core", ""]) - for filename in glob.glob(f"{root_path}/**/*.pyx", recursive=True): - mod = filename[len(root_path) : -4] - if sys.platform == "win32" and mod.replace(os.path.sep, "/") in _posix_only_modules: + root_path = Path("cuda", "core") + for filename in glob.glob(str(root_path / "**" / "*.pyx"), recursive=True): + # Module names are always spelled POSIX-style, on every platform. + mod = Path(filename).relative_to(root_path).with_suffix("").as_posix() + if sys.platform == "win32" and mod in _posix_only_modules: continue yield mod @@ -176,12 +177,12 @@ def get_sources(mod_name): # Add module-specific .cpp file from _cpp/ directory if it exists # Example: _resource_handles.pyx finds _cpp/resource_handles.cpp. cpp_file = f"cuda/core/_cpp/{mod_name.lstrip('_')}.cpp" - if os.path.exists(cpp_file): + if Path(cpp_file).exists(): sources.append(cpp_file) return sources - all_include_dirs = [os.path.join(cuda_path, "include")] + all_include_dirs = [str(Path(cuda_path, "include"))] extra_compile_args = [] extra_link_args = [] extra_cythonize_kwargs = {} @@ -205,7 +206,7 @@ def get_sources(mod_name): ext_modules = tuple( Extension( - f"cuda.core.{mod.replace(os.path.sep, '.')}", + f"cuda.core.{mod.replace('/', '.')}", sources=get_sources(mod), include_dirs=[ "cuda/core/_include", @@ -239,7 +240,7 @@ def get_sources(mod_name): return -def _add_cython_include_paths_to_pth(wheel_path: str) -> None: +def _add_cython_include_paths_to_pth(wheel_path: Path) -> None: """ Modify the .pth file in an editable install wheel to add Cython include paths. @@ -270,7 +271,7 @@ def _add_cython_include_paths_to_pth(wheel_path: str) -> None: # Create a temporary directory for wheel manipulation with tempfile.TemporaryDirectory() as tmpdir: tmpdir_path = Path(tmpdir) - wheel_file = Path(wheel_path) + wheel_file = wheel_path # Extract the wheel extract_dir = tmpdir_path / "extracted" @@ -325,7 +326,7 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non wheel_name = _build_meta.build_editable(wheel_directory, config_settings, metadata_directory) # Patch the .pth file to add Cython include paths - wheel_path = os.path.join(wheel_directory, wheel_name) + wheel_path = Path(wheel_directory, wheel_name) _add_cython_include_paths_to_pth(wheel_path) return wheel_name diff --git a/cuda_core/cuda/core/_program.pyi b/cuda_core/cuda/core/_program.pyi index d046523b007..28667f96fec 100644 --- a/cuda_core/cuda/core/_program.pyi +++ b/cuda_core/cuda/core/_program.pyi @@ -8,6 +8,7 @@ This module provides :class:`Program` for compiling source code into from __future__ import annotations from dataclasses import dataclass +from os import PathLike from cuda.bindings import nvrtc from cuda.core._linker import LinkerHandleT @@ -141,6 +142,12 @@ class Program: class ProgramOptions: """Customizable options for configuring :class:`Program`. + Every path-valued option (``include_path``, ``pre_include``, ``create_pch``, + ``use_pch``, ``pch_dir``, ``fdevice_time_trace``) accepts either a :class:`str` + or any :class:`os.PathLike`, and stores it as a :class:`pathlib.Path`. Callers + building paths with :mod:`pathlib` therefore never need to convert back to + ``str``. + Attributes ---------- name : str, optional @@ -207,11 +214,11 @@ class ProgramOptions: undefine_macro : Union[str, list[str]], optional Cancel any previous definition of a macro, or list of macros. Default: None - include_path : Union[str, list[str]], optional + include_path : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional Add the directory or directories to the list of directories to be searched for headers. Default: None - pre_include : Union[str, list[str]], optional - Preinclude one or more headers during preprocessing. Can be either a string or a list of strings. + pre_include : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional + Preinclude one or more headers during preprocessing. Can be either a single path or a list of paths. Default: None no_source_include : bool, optional Disable the default behavior of adding the directory of each input source to the include path. @@ -270,7 +277,7 @@ class ProgramOptions: no_cache : bool, optional Disable compiler caching. Default: False - fdevice_time_trace : str, optional + fdevice_time_trace : Union[str, os.PathLike], optional Generate time trace JSON for profiling compilation (NVRTC only). Default: None device_float128 : bool, optional @@ -285,13 +292,13 @@ class ProgramOptions: pch : bool, optional Use default precompiled header (NVRTC only, CUDA 12.8+). Default: False - create_pch : str, optional + create_pch : Union[str, os.PathLike], optional Create precompiled header file (NVRTC only, CUDA 12.8+). Default: None - use_pch : str, optional + use_pch : Union[str, os.PathLike], optional Use specific precompiled header file (NVRTC only, CUDA 12.8+). Default: None - pch_dir : str, optional + pch_dir : Union[str, os.PathLike], optional PCH directory location (NVRTC only, CUDA 12.8+). Default: None pch_verbose : bool, optional @@ -333,8 +340,8 @@ class ProgramOptions: gen_opt_lto: bool | None = None define_macro: str | tuple[str, str] | list[str | tuple[str, str]] | tuple[str | tuple[str, str], ...] | None = None undefine_macro: str | list[str] | tuple[str] | None = None - include_path: str | list[str] | tuple[str] | None = None - pre_include: str | list[str] | tuple[str] | None = None + include_path: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None + pre_include: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None no_source_include: bool | None = None std: str | None = None builtin_move_forward: bool | None = None @@ -354,14 +361,14 @@ class ProgramOptions: fdevice_syntax_only: bool | None = None minimal: bool | None = None no_cache: bool | None = None - fdevice_time_trace: str | None = None + fdevice_time_trace: str | PathLike[str] | None = None device_float128: bool | None = None frandom_seed: str | None = None ofast_compile: str | None = None pch: bool | None = None - create_pch: str | None = None - use_pch: str | None = None - pch_dir: str | None = None + create_pch: str | PathLike[str] | None = None + use_pch: str | PathLike[str] | None = None + pch_dir: str | PathLike[str] | None = None pch_verbose: bool | None = None pch_messages: bool | None = None instantiate_templates_in_pch: bool | None = None @@ -418,12 +425,23 @@ class ProgramOptions: """Convert extra_sources to bytes format for NVVM.""" __all__ = ['Program', 'ProgramOptions'] ProgramHandleT = nvrtc.nvrtcProgram | int | LinkerHandleT +_PATH_OPTION_FIELDS = ('include_path', 'pre_include', 'create_pch', 'use_pch', 'pch_dir', 'fdevice_time_trace') _nvvm_module = None _nvvm_import_attempted = False def _can_load_generated_ptx() -> bool: """Check if the driver can load PTX generated by the current NVRTC version.""" +def _coerce_path_option(value): + """Normalize a path-valued :class:`ProgramOptions` field. + + ``str`` / :class:`os.PathLike` becomes :class:`pathlib.Path`; a ``list`` + or ``tuple`` has its ``str`` / :class:`os.PathLike` items converted while + keeping the container type. Anything else is returned unchanged, so the + "silently ignored at compile time" behavior of non-path values (``False``, + ``range(...)``, ...) is unaffected. + """ + def _program_compile_uncached(program, target_type, name_expressions, logs): """Run ``Program_compile`` without the cache wrapper. diff --git a/cuda_core/cuda/core/_program.pyx b/cuda_core/cuda/core/_program.pyx index 7fb099b06d2..7f0d91e9d41 100644 --- a/cuda_core/cuda/core/_program.pyx +++ b/cuda_core/cuda/core/_program.pyx @@ -10,6 +10,8 @@ This module provides :class:`Program` for compiling source code into from __future__ import annotations from dataclasses import dataclass +from os import PathLike +from pathlib import Path import threading from typing import TYPE_CHECKING from warnings import warn @@ -294,6 +296,12 @@ cdef class Program: class ProgramOptions: """Customizable options for configuring :class:`Program`. + Every path-valued option (``include_path``, ``pre_include``, ``create_pch``, + ``use_pch``, ``pch_dir``, ``fdevice_time_trace``) accepts either a :class:`str` + or any :class:`os.PathLike`, and stores it as a :class:`pathlib.Path`. Callers + building paths with :mod:`pathlib` therefore never need to convert back to + ``str``. + Attributes ---------- name : str, optional @@ -360,11 +368,11 @@ class ProgramOptions: undefine_macro : Union[str, list[str]], optional Cancel any previous definition of a macro, or list of macros. Default: None - include_path : Union[str, list[str]], optional + include_path : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional Add the directory or directories to the list of directories to be searched for headers. Default: None - pre_include : Union[str, list[str]], optional - Preinclude one or more headers during preprocessing. Can be either a string or a list of strings. + pre_include : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional + Preinclude one or more headers during preprocessing. Can be either a single path or a list of paths. Default: None no_source_include : bool, optional Disable the default behavior of adding the directory of each input source to the include path. @@ -423,7 +431,7 @@ class ProgramOptions: no_cache : bool, optional Disable compiler caching. Default: False - fdevice_time_trace : str, optional + fdevice_time_trace : Union[str, os.PathLike], optional Generate time trace JSON for profiling compilation (NVRTC only). Default: None device_float128 : bool, optional @@ -438,13 +446,13 @@ class ProgramOptions: pch : bool, optional Use default precompiled header (NVRTC only, CUDA 12.8+). Default: False - create_pch : str, optional + create_pch : Union[str, os.PathLike], optional Create precompiled header file (NVRTC only, CUDA 12.8+). Default: None - use_pch : str, optional + use_pch : Union[str, os.PathLike], optional Use specific precompiled header file (NVRTC only, CUDA 12.8+). Default: None - pch_dir : str, optional + pch_dir : Union[str, os.PathLike], optional PCH directory location (NVRTC only, CUDA 12.8+). Default: None pch_verbose : bool, optional @@ -487,8 +495,8 @@ class ProgramOptions: gen_opt_lto: bool | None = None define_macro: str | tuple[str, str] | list[str | tuple[str, str]] | tuple[str | tuple[str, str], ...] | None = None undefine_macro: str | list[str] | tuple[str] | None = None - include_path: str | list[str] | tuple[str] | None = None - pre_include: str | list[str] | tuple[str] | None = None + include_path: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None + pre_include: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None no_source_include: bool | None = None std: str | None = None builtin_move_forward: bool | None = None @@ -508,14 +516,14 @@ class ProgramOptions: fdevice_syntax_only: bool | None = None minimal: bool | None = None no_cache: bool | None = None - fdevice_time_trace: str | None = None + fdevice_time_trace: str | PathLike[str] | None = None device_float128: bool | None = None frandom_seed: str | None = None ofast_compile: str | None = None pch: bool | None = None - create_pch: str | None = None - use_pch: str | None = None - pch_dir: str | None = None + create_pch: str | PathLike[str] | None = None + use_pch: str | PathLike[str] | None = None + pch_dir: str | PathLike[str] | None = None pch_verbose: bool | None = None pch_messages: bool | None = None instantiate_templates_in_pch: bool | None = None @@ -528,6 +536,12 @@ class ProgramOptions: if self.name is None: self.name = "default_program" self._name = self.name.encode() + # Path-valued options accept str or os.PathLike; normalize to Path so + # callers never have to convert back to str just to build options. + for _field in _PATH_OPTION_FIELDS: + _value = getattr(self, _field) + if _value is not None: + setattr(self, _field, _coerce_path_option(_value)) # Set arch to default if not provided if self.arch is None: self.arch = f"sm_{Device().arch}" @@ -632,6 +646,42 @@ class ProgramOptions: # ============================================================================= +# ``ProgramOptions`` fields that name a filesystem path. ``str`` and +# ``os.PathLike`` values for these are normalized to :class:`pathlib.Path` in +# ``ProgramOptions.__post_init__`` so everything downstream sees one type. +# +# ``name`` is not here: NVRTC uses it as the source *filename*, but it is a +# plain label (``ProgramOptions.__post_init__`` encodes it, and the program +# cache inspects it for a directory component), so it stays ``str``. ``time`` +# is not here either: NVRTC treats it as an output filename, but the same +# field is forwarded to ``LinkerOptions.time`` (a bool flag) for PTX inputs. +_PATH_OPTION_FIELDS = ( + "include_path", + "pre_include", + "create_pch", + "use_pch", + "pch_dir", + "fdevice_time_trace", +) + + +def _coerce_path_option(value): + """Normalize a path-valued :class:`ProgramOptions` field. + + ``str`` / :class:`os.PathLike` becomes :class:`pathlib.Path`; a ``list`` + or ``tuple`` has its ``str`` / :class:`os.PathLike` items converted while + keeping the container type. Anything else is returned unchanged, so the + "silently ignored at compile time" behavior of non-path values (``False``, + ``range(...)``, ...) is unaffected. + """ + if isinstance(value, (str, PathLike)): + return Path(value) + if isinstance(value, (list, tuple)): + coerced = [Path(v) if isinstance(v, (str, PathLike)) else v for v in value] + return tuple(coerced) if isinstance(value, tuple) else coerced + return value + + def _program_compile_uncached(program, target_type, name_expressions, logs): """Run ``Program_compile`` without the cache wrapper. @@ -1129,13 +1179,15 @@ cdef inline list _prepare_nvrtc_options_impl(object opts): for macro in opts.undefine_macro: options.append(f"--undefine-macro={macro}") if opts.include_path is not None: - if isinstance(opts.include_path, str): + # ``__post_init__`` turns str/PathLike into Path, but the dataclass is + # mutable, so accept either form here. + if isinstance(opts.include_path, (str, PathLike)): options.append(f"--include-path={opts.include_path}") elif is_sequence(opts.include_path): for path in opts.include_path: options.append(f"--include-path={path}") if opts.pre_include is not None: - if isinstance(opts.pre_include, str): + if isinstance(opts.pre_include, (str, PathLike)): options.append(f"--pre-include={opts.pre_include}") elif is_sequence(opts.pre_include): for header in opts.pre_include: diff --git a/cuda_core/cuda/core/utils/_program_cache/_keys.py b/cuda_core/cuda/core/utils/_program_cache/_keys.py index e170bc18131..c2fda655f91 100644 --- a/cuda_core/cuda/core/utils/_program_cache/_keys.py +++ b/cuda_core/cuda/core/utils/_program_cache/_keys.py @@ -14,6 +14,7 @@ import abc import collections.abc import hashlib +import os from typing import Any, Callable, Sequence # Mutual-dependency contract: this module imports ProgramOptions from @@ -269,7 +270,8 @@ def _option_is_set(options: ProgramOptions, name: str) -> bool: - Boolean flags (``pch``): truthy only. - str-or-sequence fields (``include_path``, ``pre_include``): ``str`` - (including empty) or a non-empty ``collections.abc.Sequence`` (list, + (including empty), ``os.PathLike`` (``ProgramOptions`` normalizes both + to ``pathlib.Path``), or a non-empty ``collections.abc.Sequence`` (list, tuple, range, user subclass, ...); everything else (``False``, ``int``, empty sequence, ``None``) is ignored by the compiler and must not trigger a cache-time guard. @@ -284,11 +286,12 @@ def _option_is_set(options: ProgramOptions, name: str) -> bool: if name in _BOOLEAN_OPTION_FIELDS: return bool(value) if name in _STR_OR_SEQUENCE_OPTION_FIELDS: - # Mirror ``_prepare_nvrtc_options_impl``: it checks ``isinstance(v, str)`` - # first, then ``is_sequence(v)`` (which is ``isinstance(v, Sequence)``). - # We therefore accept any ``collections.abc.Sequence`` (range, deque, - # user subclass, etc.), not just list/tuple. - if isinstance(value, str): + # Mirror ``_prepare_nvrtc_options_impl``: it checks + # ``isinstance(v, (str, os.PathLike))`` first, then ``is_sequence(v)`` + # (which is ``isinstance(v, Sequence)``). We therefore accept any + # ``collections.abc.Sequence`` (range, deque, user subclass, etc.), + # not just list/tuple. + if isinstance(value, (str, os.PathLike)): return True if isinstance(value, collections.abc.Sequence): return len(value) > 0 diff --git a/cuda_core/docs/source/release/1.2.0-notes.rst b/cuda_core/docs/source/release/1.2.0-notes.rst index 120d2c2a253..eec93dfe880 100644 --- a/cuda_core/docs/source/release/1.2.0-notes.rst +++ b/cuda_core/docs/source/release/1.2.0-notes.rst @@ -73,6 +73,14 @@ Fixes and enhancements Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted. (`#2439 `__) +- The path-valued :class:`ProgramOptions` fields (``include_path``, + ``pre_include``, ``create_pch``, ``use_pch``, ``pch_dir``, + ``fdevice_time_trace``) now accept any :class:`os.PathLike` in addition to + ``str``, and normalize what they are given to :class:`pathlib.Path`. Code + that builds paths with :mod:`pathlib` no longer has to convert back to + ``str``. + (`#2500 `__) + Deprecation Notices ------------------- diff --git a/cuda_core/examples/thread_block_cluster.py b/cuda_core/examples/thread_block_cluster.py index 078407ac6be..8d00c1f5748 100644 --- a/cuda_core/examples/thread_block_cluster.py +++ b/cuda_core/examples/thread_block_cluster.py @@ -14,8 +14,8 @@ # dependencies = ["cuda_bindings", "cuda_core"] # /// -import os import sys +from pathlib import Path import numpy as np @@ -74,13 +74,13 @@ def main(): if cuda_path is None: print("This example requires CUDA_PATH or CUDA_HOME to point to a CUDA toolkit.", file=sys.stderr) sys.exit(1) - cuda_include = os.path.join(cuda_path, "include") - if not os.path.isdir(cuda_include): + cuda_include = Path(cuda_path, "include") + if not cuda_include.is_dir(): print(f"CUDA include directory not found: {cuda_include}", file=sys.stderr) sys.exit(1) include_path = [cuda_include] - cccl_include = os.path.join(cuda_include, "cccl") - if os.path.isdir(cccl_include): + cccl_include = cuda_include / "cccl" + if cccl_include.is_dir(): include_path.insert(0, cccl_include) dev = Device() diff --git a/cuda_core/examples/tma_tensor_map.py b/cuda_core/examples/tma_tensor_map.py index 8c048d4a15d..db719297195 100644 --- a/cuda_core/examples/tma_tensor_map.py +++ b/cuda_core/examples/tma_tensor_map.py @@ -26,8 +26,8 @@ # dependencies = ["cuda_bindings", "cuda_core>0.6.0", "cupy-cuda13x"] # /// -import os import sys +from pathlib import Path import cupy as cp import numpy as np @@ -113,14 +113,14 @@ def _get_cccl_include_paths(): print("This example requires CUDA_PATH or CUDA_HOME to point to a CUDA toolkit.", file=sys.stderr) sys.exit(1) - cuda_include = os.path.join(cuda_path, "include") - if not os.path.isdir(cuda_include): + cuda_include = Path(cuda_path, "include") + if not cuda_include.is_dir(): print(f"CUDA include directory not found: {cuda_include}", file=sys.stderr) sys.exit(1) include_path = [cuda_include] - cccl_include = os.path.join(cuda_include, "cccl") - if os.path.isdir(cccl_include): + cccl_include = cuda_include / "cccl" + if cccl_include.is_dir(): include_path.insert(0, cccl_include) return include_path diff --git a/cuda_core/tests/example_tests/test_basic_examples.py b/cuda_core/tests/example_tests/test_basic_examples.py index bf423758366..b98e04c20bc 100644 --- a/cuda_core/tests/example_tests/test_basic_examples.py +++ b/cuda_core/tests/example_tests/test_basic_examples.py @@ -3,18 +3,18 @@ # If we have subcategories of examples in the future, this file can be split along those lines -import glob import os import platform import subprocess import sys import warnings +from pathlib import Path import pytest -from cuda_python_test_helpers.pep723 import has_package_requirements_or_skip from cuda.core import Device, ManagedMemoryResource, system from cuda.core._program import _can_load_generated_ptx +from cuda_python_test_helpers.pep723 import has_package_requirements_or_skip def has_compute_capability_9_or_higher() -> bool: @@ -91,14 +91,14 @@ def has_recent_memory_pool_support() -> bool: } -samples_path = os.path.join(os.path.dirname(__file__), "..", "..", "examples") -sample_files = [os.path.basename(x) for x in glob.glob(samples_path + "**/*.py", recursive=True)] +samples_path = Path(__file__).parents[2] / "examples" +sample_files = [x.name for x in samples_path.glob("**/*.py")] @pytest.mark.parametrize("example", sample_files) @pytest.mark.parallel_threads_limit(8) def test_example(example): - example_path = os.path.join(samples_path, example) + example_path = samples_path / example has_package_requirements_or_skip(example_path) system_requirement = SYSTEM_REQUIREMENTS.get(example, lambda: True) diff --git a/cuda_core/tests/helpers/__init__.py b/cuda_core/tests/helpers/__init__.py index 2305cfaa1e5..64c94813677 100644 --- a/cuda_core/tests/helpers/__init__.py +++ b/cuda_core/tests/helpers/__init__.py @@ -2,7 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 import functools -import os +from pathlib import Path +from typing import Union from cuda.core._utils.cuda_utils import handle_return from cuda.pathfinder import get_cuda_path_or_home @@ -12,12 +13,12 @@ CUDA_INCLUDE_PATH = None CCCL_INCLUDE_PATHS = None if CUDA_PATH is not None: - path = os.path.join(CUDA_PATH, "include") - if os.path.isdir(path): + path = Path(CUDA_PATH, "include") + if path.is_dir(): CUDA_INCLUDE_PATH = path CCCL_INCLUDE_PATHS = (path,) - path = os.path.join(path, "cccl") - if os.path.isdir(path): + path = path / "cccl" + if path.is_dir(): CCCL_INCLUDE_PATHS = (path,) + CCCL_INCLUDE_PATHS diff --git a/cuda_core/tests/test_program.py b/cuda_core/tests/test_program.py index 28465425c0e..0c794291239 100644 --- a/cuda_core/tests/test_program.py +++ b/cuda_core/tests/test_program.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 import contextlib +import pathlib import re import warnings @@ -745,6 +746,33 @@ def test_program_options_as_bytes_nvvm(): assert "-opt=3" in options_str +@pytest.mark.parametrize("include_path", ["/usr/local/include", pathlib.Path("/usr/local/include")]) +def test_program_options_path_option_accepts_str_or_path(include_path): + """Path-valued options take str or os.PathLike and normalize to Path.""" + expected = pathlib.Path("/usr/local/include") + options = ProgramOptions(arch="sm_80", include_path=include_path) + assert options.include_path == expected + assert f"--include-path={expected}".encode() in options.as_bytes("nvrtc") + + +def test_program_options_path_option_sequence_accepts_str_or_path(): + """A list may mix str and os.PathLike; every entry becomes a Path.""" + first, second = pathlib.Path("/opt/first"), pathlib.Path("/opt/second") + options = ProgramOptions(arch="sm_80", include_path=[str(first), second]) + assert options.include_path == [first, second] + emitted = [opt.decode() for opt in options.as_bytes("nvrtc")] + assert f"--include-path={first}" in emitted + assert f"--include-path={second}" in emitted + + +def test_program_options_non_path_values_are_left_alone(): + """Values the compiler silently ignores must not be coerced (or Path() + would raise on them); only str / os.PathLike entries are converted.""" + options = ProgramOptions(arch="sm_80", include_path=False, use_pch=False) + assert options.include_path is False + assert options.use_pch is False + + def test_program_options_as_bytes_invalid_backend(): """Test ProgramOptions.as_bytes() with invalid backend""" options = ProgramOptions(arch="sm_80") diff --git a/cuda_core/tests/test_program_cache.py b/cuda_core/tests/test_program_cache.py index a8d3fc85f7e..790319d828c 100644 --- a/cuda_core/tests/test_program_cache.py +++ b/cuda_core/tests/test_program_cache.py @@ -840,9 +840,10 @@ def _named_failure(self, *args, **kwargs): # Non-list/tuple Sequence: the compiler iterates it via ``is_sequence`` # (``isinstance(v, Sequence)``), so the guard must too. pytest.param({"include_path": range(1)}, id="include_path_nonempty_range"), - # Empty-string path-like options -- NVRTC still emits a flag - # (``--use-pch=``, ``--pch-dir=``, ``--pre-include=``) so the guard - # must fire for them too. + # Empty-string path-like options -- ``ProgramOptions`` normalizes + # these to ``Path("")`` (i.e. ``Path(".")``) and NVRTC still emits a + # flag (``--use-pch=.``, ``--pch-dir=.``, ``--pre-include=.``), so the + # guard must fire for them too. pytest.param({"use_pch": ""}, id="use_pch_empty_string"), pytest.param({"pch_dir": ""}, id="pch_dir_empty_string"), pytest.param({"pre_include": ""}, id="pre_include_empty_string"),