Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 16 additions & 77 deletions cfd_python/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""CFD Python - Python bindings for CFD simulation library
"""CFD Python - Python bindings for CFD simulation library.

This package provides Python bindings for the C-based CFD simulation library,
enabling high-performance computational fluid dynamics simulations from Python.
Expand All @@ -16,26 +16,17 @@
- OUTPUT_CSV_STATISTICS: Global statistics (CSV)
"""

# Get version from package metadata (setuptools-scm) or fall back to C module
try:
from importlib.metadata import PackageNotFoundError, version
from ._version import get_version

try:
__version__ = version("cfd-python")
except PackageNotFoundError:
# Package not installed, try C module version
__version__ = None
except ImportError:
# Fallback for unusual environments where importlib.metadata is unavailable
__version__ = None
__version__ = get_version()

# Core exports that are always available
# Core exports list (for documentation and dev mode)
_CORE_EXPORTS = [
# Simulation functions
"run_simulation",
"run_simulation_with_params",
"create_grid",
"get_default_solver_params",
"run_simulation_with_params",
# Solver functions
"list_solvers",
"has_solver",
Expand All @@ -54,71 +45,19 @@
"OUTPUT_CSV_STATISTICS",
]

# Load C extension and populate module namespace
try:
# Import the C extension module to access dynamic solver constants
from . import cfd_python as _cfd_module
from .cfd_python import (
OUTPUT_CSV_CENTERLINE,
OUTPUT_CSV_STATISTICS,
OUTPUT_CSV_TIMESERIES,
OUTPUT_FULL_FIELD,
# Output type constants
OUTPUT_PRESSURE,
OUTPUT_VELOCITY,
create_grid,
get_default_solver_params,
get_solver_info,
has_solver,
# Solver functions
list_solvers,
# Simulation functions
run_simulation,
run_simulation_with_params,
# Output functions
set_output_dir,
write_csv_timeseries,
write_vtk_scalar,
write_vtk_vector,
)

# Fall back to C module version if metadata lookup failed
if __version__ is None:
__version__ = getattr(_cfd_module, "__version__", "0.0.0")

# Dynamically export all SOLVER_* constants from the C module
# This allows new solvers to be automatically available without
# updating this file
_solver_constants = []
for name in dir(_cfd_module):
if name.startswith("SOLVER_"):
globals()[name] = getattr(_cfd_module, name)
_solver_constants.append(name)

# Build complete __all__ list
__all__ = _CORE_EXPORTS + _solver_constants
from ._loader import ExtensionNotBuiltError, load_extension

except ImportError as e:
# Check if this is a development environment (source checkout without built extension)
# vs a broken installation (extension exists but fails to load)
import os as _os
_exports, _solver_constants = load_extension()

_package_dir = _os.path.dirname(__file__)
# Add all exports to module namespace
globals().update(_exports)
globals().update(_solver_constants)

# Look for compiled extension files
_extension_exists = any(
f.startswith("cfd_python") and (f.endswith(".pyd") or f.endswith(".so"))
for f in _os.listdir(_package_dir)
)
# Build __all__ with core exports + dynamic solver constants
__all__ = _CORE_EXPORTS + list(_solver_constants.keys())

if _extension_exists:
# Extension file exists but failed to load - this is an error
raise ImportError(
f"Failed to load cfd_python C extension: {e}\n"
"The extension file exists but could not be imported. "
"This may indicate a missing dependency or ABI incompatibility."
) from e
else:
# Development mode - module not yet built
__all__ = _CORE_EXPORTS
if __version__ is None:
__version__ = "0.0.0-dev"
except ExtensionNotBuiltError:
# Development mode - extension not built (this is expected)
__all__ = _CORE_EXPORTS
100 changes: 100 additions & 0 deletions cfd_python/_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""C extension loader with error handling for cfd_python."""

import os

__all__ = ["load_extension", "ExtensionNotBuiltError"]


class ExtensionNotBuiltError(ImportError):
"""Raised when C extension is not built (development mode)."""

pass


def _check_extension_exists() -> bool:
"""Check if compiled extension files exist in package directory."""
package_dir = os.path.dirname(__file__)
return any(
f.startswith("cfd_python") and (f.endswith(".pyd") or f.endswith(".so"))
for f in os.listdir(package_dir)
)


def load_extension():
"""Load the C extension module and return exports.

Returns:
tuple: (exports_dict, solver_constants)

Raises:
ImportError: If extension exists but fails to load
ExtensionNotBuiltError: If extension is not built (dev mode)
"""
try:
from . import cfd_python as _cfd_module
from .cfd_python import (
OUTPUT_CSV_CENTERLINE,
OUTPUT_CSV_STATISTICS,
OUTPUT_CSV_TIMESERIES,
OUTPUT_FULL_FIELD,
OUTPUT_PRESSURE,
OUTPUT_VELOCITY,
create_grid,
get_default_solver_params,
get_solver_info,
has_solver,
list_solvers,
run_simulation,
run_simulation_with_params,
set_output_dir,
write_csv_timeseries,
write_vtk_scalar,
write_vtk_vector,
)

# Collect all exports
exports = {
# Simulation functions
"run_simulation": run_simulation,
"run_simulation_with_params": run_simulation_with_params,
"create_grid": create_grid,
"get_default_solver_params": get_default_solver_params,
# Solver functions
"list_solvers": list_solvers,
"has_solver": has_solver,
"get_solver_info": get_solver_info,
# Output functions
"set_output_dir": set_output_dir,
"write_vtk_scalar": write_vtk_scalar,
"write_vtk_vector": write_vtk_vector,
"write_csv_timeseries": write_csv_timeseries,
# Output type constants
"OUTPUT_PRESSURE": OUTPUT_PRESSURE,
"OUTPUT_VELOCITY": OUTPUT_VELOCITY,
"OUTPUT_FULL_FIELD": OUTPUT_FULL_FIELD,
"OUTPUT_CSV_TIMESERIES": OUTPUT_CSV_TIMESERIES,
"OUTPUT_CSV_CENTERLINE": OUTPUT_CSV_CENTERLINE,
"OUTPUT_CSV_STATISTICS": OUTPUT_CSV_STATISTICS,
}

# Collect dynamic SOLVER_* constants
solver_constants = {}
for name in dir(_cfd_module):
if name.startswith("SOLVER_"):
solver_constants[name] = getattr(_cfd_module, name)

return exports, solver_constants

except ImportError as e:
if _check_extension_exists():
# Extension file exists but failed to load - this is an error
raise ImportError(
f"Failed to load cfd_python C extension: {e}\n"
"The extension file exists but could not be imported. "
"This may indicate a missing dependency or ABI incompatibility."
) from e
else:
# Development mode - module not yet built
raise ExtensionNotBuiltError(
"C extension not built. Run 'pip install -e .' to build."
) from e
Comment on lines +14 to +100
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The load_extension() function and _check_extension_exists() function introduce new error handling logic but lack direct test coverage. While test_import_handling.py tests similar behavior at the module level, these internal functions are not directly tested.

Consider adding tests that:

  • Test _check_extension_exists() with various directory states (no files, .so files, .pyd files, etc.)
  • Test load_extension() behavior when extension import succeeds
  • Test ExtensionNotBuiltError is raised when extension doesn't exist
  • Test ImportError is raised with appropriate message when extension exists but fails to load

Copilot uses AI. Check for mistakes.
28 changes: 28 additions & 0 deletions cfd_python/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Version detection for cfd_python package."""

__all__ = ["get_version"]


def get_version() -> str:
"""Get package version from metadata or C module."""
# Try importlib.metadata first (works when package is installed)
try:
from importlib.metadata import PackageNotFoundError, version

try:
return version("cfd-python")
except PackageNotFoundError:
pass # Package not installed via pip, try C module next
except ImportError:
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except ImportError:
except ImportError:
# importlib.metadata not available; fallback to C module version below

Copilot uses AI. Check for mistakes.
pass # Python < 3.8 without importlib_metadata backport

# Try C module version (works when extension is built)
try:
from . import cfd_python as _cfd_module

return getattr(_cfd_module, "__version__", "0.0.0")
except ImportError:
pass # Extension not built yet (development mode)

# Fallback for development mode without extension
return "0.0.0-dev"
Comment on lines +6 to +28
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The new get_version() function and load_extension() function introduce new behavior and error handling logic but lack direct test coverage. The existing tests in test_import_handling.py and test_module.py test the overall module behavior, but don't test the internal _version.py and _loader.py modules directly.

Consider adding tests that:

  • Test get_version() with mocked importlib.metadata scenarios
  • Test load_extension() directly with mocked extension states
  • Test ExtensionNotBuiltError is raised appropriately
  • Test _check_extension_exists() with various file patterns

Copilot uses AI. Check for mistakes.
Loading
Loading