diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a54524e05..9f6f9b698 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,6 +7,7 @@ on: - ".github/workflows/docs.yml" - "README.md" - "docs/**" + - "docs_theme/**" - "mkdocs.yml" - "pyproject.toml" - "tests/docs/**" @@ -16,6 +17,7 @@ on: - ".github/workflows/docs.yml" - "README.md" - "docs/**" + - "docs_theme/**" - "mkdocs.yml" - "pyproject.toml" - "tests/docs/**" diff --git a/README.md b/README.md index 95cb2835b..d7226710e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # x2py -x2py generates importable Python extensions from Fortran sources, extracts -native declarations into language-neutral semantic IR, emits editable `.pyi` -interfaces, and reports unsupported or incomplete contracts before code -generation. +**Turn Fortran into natural Python APIs.** + +Build clean, importable native extensions from supported Fortran without +writing low-level binding code. x2py preserves modules, derived types, arrays, +and native behavior, and generates an editable `.pyi` contract so you can +shape the Python API. [Read the documentation](https://pynumlab.github.io/x2py/) for installation, the user guide, examples, and reference material. -## Installation & Quick Start - -Requires **Python 3.10+**. +The complete example below builds with one command: ```bash -# Install in development mode -pip install -e . - -# See all available commands -python3 -m x2py --help +python3 -m x2py points.f90 --out geometry ``` -Expected result: the install command completes successfully, and `--help` -prints the CLI usage with input selection, wrapper builds, `.pyi` contracts, -verbose mode, and output options. +## See it in action -The default user-facing action for a single Fortran source is to build a Python -extension. Create `scale.f90` with this input: +Create `points.f90`: - + ```fortran -real(8) function scale(value, factor) result(output) - real(8), intent(in) :: value - real(8), intent(in) :: factor - output = value * factor -end function scale -``` +module points + implicit none -The concise `python3 -m x2py --help` examples reuse this `scale.f90` -source. Help entries labeled "README Quick Start" refer to the complete source -and workflow in this section. + type :: point + real(8) :: x = 0.0d0 + real(8) :: y = 0.0d0 + end type point -Build it with the default output locations: +contains -```bash -python3 -m x2py scale.f90 -``` + subroutine move(item, dx, dy) + type(point), intent(inout) :: item + real(8), intent(in) :: dx, dy + item%x = item%x + dx + item%y = item%y + dy + end subroutine move -By default, x2py writes generated build artifacts, including the ABI-suffixed -extension, under `__x2py__/` in the directory where you run the command. It -also creates a stable `scale.so` import alias alongside your source file, so -you can simply `import scale`. + real(8) function norm_squared(item) result(value) + type(point), intent(in) :: item + value = item%x * item%x + item%y * item%y + end function norm_squared -```bash -. - scale.f90 - scale.so - __x2py__/ - scale..so - generated-wrapper sources - binding_support/ +end module points ``` -Name the Python extension and final `.so` explicitly with `--out NAME`: +**Generated Python API:** -```bash -python3 -m x2py scale.f90 --out SCALE -``` +```python +import numpy as np +import geometry.points as points -Expected result: +item = points.point(x=np.float64(3.0), y=np.float64(4.0)) +points.move(item, np.float64(1.0), np.float64(-2.0)) -```text -. - scale.f90 - SCALE.so - __x2py__/ - SCALE..so - generated-wrapper sources - binding_support/ +print(item.x, item.y) # 4.0 2.0 +print(points.norm_squared(item)) # 20.0 ``` -For a wrapper build, `--out SCALE` selects the Python module name and the final -shared-library filename. This first example is a standalone procedure, so it is -exposed directly at the extension root. +No manual bindings are required. From this source, x2py creates a Python +namespace, a class with accessible fields, a mutating procedure, and a +function. -Use `--out-dir` when you want the ABI-specific shared library and generated -intermediates in an explicit build directory: +Want a different Python API? Edit the generated `.pyi` contract to rename or +hide exports, flatten namespaces, define constructors and methods, or create +overloads. The +[contract guide](https://pynumlab.github.io/x2py/user/reference/pyi-contracts/) +shows the available edits. -```bash -python3 -m x2py scale.f90 \ - --out SCALE \ - --out-dir build/SCALE -``` +## Key Features -Expected result: +- Fortran modules exposed as Python namespaces and derived types as classes +- NumPy arrays with explicit dtype, shape, and layout checks +- Allocatable and pointer arrays with explicit lifetime operations +- Immediate Python callbacks and overloaded interfaces +- Editable `.pyi` contracts and readable generated docstrings +- Early, clear errors when a boundary cannot be wrapped -```text -. - SCALE.so - build/SCALE/ - SCALE..so - generated-wrapper sources - binding_support/ -``` - -Generate the semantic `.pyi` contract for the same source: +## Installation & Quick Start -```bash -python3 -m x2py generate --pyi scale.f90 --out contracts -``` +x2py requires **Python 3.10 or newer**, GNU Fortran, Python development +headers, NumPy, and standard build tools. -The command writes the contract package: +Clone the repository and install x2py in a virtual environment: -```text -contracts/ - __init__.pyi +```bash +git clone https://github.com/PyNumLab/x2py.git +cd x2py +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install --upgrade pip +python3 -m pip install -e . ``` -Expected contract (`contracts/__init__.pyi`): - -```python -from x2py.contracts import Addr, Arg, Float64, external, native_call +Check the installation: -@external -@native_call([Addr(Arg(0)), Addr(Arg(1))]) -def scale( - value: Float64, - factor: Float64 -) -> Float64: ... +```bash +python3 -m x2py --help ``` -The semantic contract does not repeat Fortran `intent`. Source `intent` helps -x2py choose the generated Python arguments and results, while `@native_call` -records the exact native argument order and transport. The compiled native -procedure keeps its own `intent`; editing the `.pyi` changes the wrapper call -contract, not the native procedure declaration. - -Then build the shared library from the package-entry `.pyi` contract and the -same native implementation source: +With the `points.f90` source from above in the current directory, build the +extension: ```bash -python3 -m x2py contracts/__init__.pyi \ - --native-fortran-sources scale.f90 \ - --out SCALE \ - --out-dir build/SCALE_from_pyi +python3 -m x2py points.f90 --out geometry ``` -Use `--out NAME` with wrapper builds when you want the import name and final -`.so` filename to differ from the default inferred name. - -The `.pyi` build produces the same importable extension shape: +`--out geometry` selects the import name and the final shared-library name. +x2py places the stable import file beside the source and keeps generated build +artifacts under `__x2py__/`: ```text . - SCALE.so - build/SCALE_from_pyi/ - SCALE..so + points.f90 + geometry.so + __x2py__/ + geometry..so generated-wrapper sources binding_support/ ``` -The direct source build exposes the standalone procedure at the extension root: - -```python -import sys - -import numpy as np - -sys.path.insert(0, "build/SCALE") -import SCALE - -print(SCALE.scale(np.float64(3.0), np.float64(2.5))) # 7.5 -``` - -The package-entry `.pyi` build exposes the same Python API: - -```python -import sys - -import numpy as np +The Python code shown at the top of this README can now import `geometry` +directly. -sys.path.insert(0, "build/SCALE_from_pyi") -import SCALE +Use `--out-dir` to place the ABI-specific extension and generated files in a +chosen build directory: -print(SCALE.scale(np.float64(3.0), np.float64(2.5))) # 7.5 +```bash +python3 -m x2py points.f90 \ + --out geometry \ + --out-dir build/geometry ``` -Both calls print: - ```text -7.5 +. + geometry.so + build/geometry/ + geometry..so + generated-wrapper sources + binding_support/ ``` -For a small derived-type wrapper, create `points.f90`: +### Inspect the generated contract -```fortran -module points - implicit none - type :: point - real(8) :: x - real(8) :: y - end type point -contains - subroutine move(item, dx, dy) - type(point), intent(inout) :: item - real(8), intent(in) :: dx - real(8), intent(in) :: dy - item%x = item%x + dx - item%y = item%y + dy - end subroutine move +Generate the editable `.pyi` contract for the same `points.f90`: - real(8) function norm_squared(item) result(value) - type(point), intent(in) :: item - value = item%x * item%x + item%y * item%y - end function norm_squared -end module points +```bash +python3 -m x2py generate --pyi points.f90 --out contracts ``` -Generate its semantic contract: +The command preserves the Fortran module as a contract module: -```bash -python3 -m x2py generate --pyi points.f90 --out contracts +```text +contracts/ + __init__.pyi + points.pyi ``` -Expected contract (`contracts/points.pyi`): +Generated `contracts/points.pyi`: ```python from x2py.contracts import Addr, Arg, Float64, native_call @@ -242,12 +179,12 @@ class point: def __init__( self, *, - x: Float64 = ..., - y: Float64 = ... + x: Float64 = 0.0, + y: Float64 = 0.0 ) -> None: ... - x: Float64 - y: Float64 + x: Float64 = 0.0 + y: Float64 = 0.0 @native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) def move( @@ -261,41 +198,57 @@ def norm_squared( ) -> Float64: ... ``` -Build and import it with a clean Python module name: +The contract describes the generated Python class, fields, functions, exact +NumPy scalar types, and native argument order. Editing it changes the wrapper +API; it does not change the Fortran implementation. + +### Build from the contract + +After editing the contract, rebuild the same Python API from the package entry +and the original Fortran implementation: ```bash -python3 -m x2py points.f90 --out geometry --out-dir build/geometry +python3 -m x2py contracts/__init__.pyi \ + --native-fortran-sources points.f90 \ + --out geometry \ + --out-dir build/geometry_from_pyi ``` +The contract build has the same import name and module layout: + +```text +. + geometry.so + build/geometry_from_pyi/ + geometry..so + generated-wrapper sources + binding_support/ +``` + +Import the extension from the explicit build directory when needed: + ```python import sys import numpy as np -sys.path.insert(0, "build/geometry") -import geometry +sys.path.insert(0, "build/geometry_from_pyi") +import geometry.points as points -p = geometry.points.point(x=np.float64(3.0), y=np.float64(4.0)) -geometry.points.move(p, np.float64(1.0), np.float64(-2.0)) - -print(p.x, p.y) -print(geometry.points.norm_squared(p)) +item = points.point(x=np.float64(3.0), y=np.float64(4.0)) +points.move(item, np.float64(1.0), np.float64(-2.0)) +print(points.norm_squared(item)) # 20.0 ``` -Expected result: - -```text -4.0 2.0 -20.0 -``` +### Inspect the native build Use `--verbose` when you want to see the compiler commands and confirm which wrapper flags reached the build: ```bash -python3 -m x2py scale.f90 \ - --out SCALE_debug \ - --out-dir build/SCALE_debug \ +python3 -m x2py points.f90 \ + --out geometry_debug \ + --out-dir build/geometry_debug \ --verbose \ --compiler gfortran \ --wrapper-fortran-flags=-O2 \ @@ -309,16 +262,10 @@ The custom wrapper flags appear in the relevant command lines: ```text ... -O2 ... generated bridge ... ... -O2 ... generated Python binding ... - -shared ... -O2 ... SCALE_debug ... + -shared ... -O2 ... geometry_debug ... ``` -Standalone procedures are the smallest wrapper surface and therefore come -first. Contained Fortran module procedures are preserved under Python child -modules; continue with the -[first wrapped module](https://pynumlab.github.io/x2py/user/getting-started/first-wrapped-module/) -for that layout and for public module state. - -The runtime wrapper mechanism is: +## How it works ```text Fortran sources @@ -492,9 +439,9 @@ conversion and `.pyi` emission: from x2py import build_fortran_extension result = build_fortran_extension( - "scale.f90", - output_name="SCALE", - output_dir="build/SCALE", + "points.f90", + output_name="geometry", + output_dir="build/geometry_api", ) print(result.module_name) print(result.shared_library) @@ -520,9 +467,17 @@ X2PY_C_DOCS_END --> For native projects with macros, includes, or target flags, use the compiler-preprocessed CLI path or an equivalent preprocessing configuration. +## Development + +Run the full suite from the repository root: + +```bash +PYTHONPATH=. python3 -m pytest -q +``` + ## Documentation -- **[Documentation](https://pynumlab.github.io/x2py/)** — Complete published documentation +- **[Documentation](https://pynumlab.github.io/x2py/)** — Learn how to install and use x2py - **[Getting Started](https://pynumlab.github.io/x2py/user/getting-started/)** — Installation, verification, standalone procedures, modules, and rebuild workflow - **[User Guide](https://pynumlab.github.io/x2py/user/guide/)** — Data types, functions, modules, arrays, derived types, callbacks, ownership, and runtime behavior - -Run the full suite from the repository root: - -```bash -PYTHONPATH=. python3 -m pytest -q -``` diff --git a/docs/index.md b/docs/index.md index 5a3dc88ed..71e48a43d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- title: x2py -description: Turn Fortran into importable Python extensions with zero boilerplate +description: Turn Fortran functions, modules, arrays, and derived types into natural Python APIs audience: users prerequisites: none related: user/getting-started/index.md, user/getting-started/installation.md @@ -10,95 +10,98 @@ publication: reviewed # x2py -**x2py turns supported Fortran source into fast, importable Python extensions.** +**Turn Fortran into natural Python APIs.** -It also generates a language-neutral semantic IR and editable `.pyi` -contracts, so unsupported boundaries are reported before wrapper compilation. +Build clean, importable native extensions from supported Fortran without +writing low-level binding code. x2py preserves modules, derived types, arrays, +and native behavior, and generates an editable `.pyi` contract so you can +shape the Python API. + +The complete example below builds with one command: + +```bash +python3 -m x2py points.f90 --out geometry +``` --- -## Try it in 30 seconds {#try-x2py} +## See it in action -Create a file `scale.f90`: +Create `points.f90`: - + ```fortran -real(8) function scale(value, factor) result(output) - real(8), intent(in) :: value - real(8), intent(in) :: factor - output = value * factor -end function scale -``` +module points + implicit none -Build the Python extension: + type :: point + real(8) :: x = 0.0d0 + real(8) :: y = 0.0d0 + end type point -```bash -python3 -m x2py scale.f90 -``` +contains -Use it from Python: + subroutine move(item, dx, dy) + type(point), intent(inout) :: item + real(8), intent(in) :: dx, dy + item%x = item%x + dx + item%y = item%y + dy + end subroutine move -```python -import numpy as np -import scale + real(8) function norm_squared(item) result(value) + type(point), intent(in) :: item + value = item%x * item%x + item%y * item%y + end function norm_squared -result = scale.scale(np.float64(3.0), np.float64(2.5)) -print(result) # 7.5 +end module points ``` -Inspect the generated contract: +**Generated Python API:** ```python -print(scale.scale.__doc__) -``` +import numpy as np +import geometry.points as points -```text -scale(value, factor) -> float64 +item = points.point(x=np.float64(3.0), y=np.float64(4.0)) +points.move(item, np.float64(1.0), np.float64(-2.0)) -Parameters ----------- -value : float64 -factor : float64 +print(item.x, item.y) # 4.0 2.0 +print(points.norm_squared(item)) # 20.0 +``` -Returns -------- -result : float64 +No manual bindings are required. From this source, x2py creates a Python +namespace, a class with accessible fields, a mutating procedure, and a +function. -Raises ------- -TypeError - If an argument has an incompatible Python type or dtype. -``` +Want a different Python API? Edit the generated `.pyi` contract to rename or +hide exports, flatten namespaces, define constructors and methods, or create +overloads. The [contract guide](user/reference/pyi-contracts/index.md) shows +the available edits. --- ## How it works -1. You write standard Fortran -2. `x2py` parses the interface and generates a compact native wrapper -3. It produces a Python extension module and editable semantic `.pyi` contracts -4. You get full NumPy scalar dtype safety and clear error messages +1. Write standard Fortran. +2. Run `x2py` on the source. +3. Import the generated native extension. +4. Optionally edit the generated `.pyi` contract to shape the Python API. -No manual `f2py` signatures. No low-level boilerplate. - -## Next steps - -[Getting Started](user/getting-started/index.md){ .md-button .md-button--primary } - -This guide walks you through installation, compiler setup, and a deeper look at the generated artifacts. +No manual binding code or low-level boilerplate. --- -## Features +## Key Features -- Automatic generation of Python extensions from Fortran -- Language-neutral semantic IR -- Editable `.pyi` type stubs -- Strict NumPy dtype checking at call time -- Clean, readable `__doc__` strings -- Build artifacts isolated in `__x2py__/` +- Fortran modules exposed as Python namespaces and derived types as classes +- NumPy arrays with explicit dtype, shape, and layout checks +- Allocatable and pointer arrays with explicit lifetime operations +- Immediate Python callbacks and overloaded interfaces +- Editable `.pyi` contracts and readable generated docstrings +- Early, clear errors when a boundary cannot be wrapped --- **Ready to wrap your Fortran code?** -Start with the [Getting Started](user/getting-started/index.md) guide. + +[Getting Started Guide →](user/getting-started/index.md){ .x2py-primary-cta } diff --git a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md index ce2d30caf..1bf130982 100644 --- a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md +++ b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md @@ -594,7 +594,7 @@ summary, the exhaustive matrix, and the test tree disagree. | Status | Collected nodes | | --- | ---: | -| `wrapper-plan` | 369 | +| `wrapper-plan` | 370 | | `dual-route` | 0 | | `legacy` | 0 | | `not-applicable` | 76 | @@ -694,6 +694,7 @@ already covered by the new generator. | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py::test_reduced_entry_generates_only_reachable_module_variable_bindings` | direct wrapper/build route | semantic .pyi generation/parsing; build/compile/link orchestration; scalar inputs/results; module variables/state | `wrapper-plan` | | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py::test_scale_runtime_contract[*]` | source/generated-.pyi parity or parametrized route | semantic .pyi generation/parsing; build/compile/link orchestration; scalar inputs/results | `wrapper-plan` | | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py::test_source_named_root_discovers_and_builds_module_leaf` | direct wrapper/build route | semantic .pyi generation/parsing; build/compile/link orchestration; scalar inputs/results; module variables/state | `wrapper-plan` | +| `tests/wrapper/fortran/build_from_source/test_build_modes.py::test_documented_homepage_points_example_builds_and_imports` | direct wrapper/build route | build/compile/link orchestration; module namespace and derived-type inputs/results | `wrapper-plan` | | `tests/wrapper/fortran/build_from_source/test_build_modes.py::test_fortran_wrapper_default_module_name_does_not_collide_with_root_function` | direct wrapper/build route | build/compile/link orchestration | `wrapper-plan` | | `tests/wrapper/fortran/build_from_source/test_build_modes.py::test_fortran_wrapper_default_places_artifacts_in_invocation_directory` | direct wrapper/build route | build/compile/link orchestration | `wrapper-plan` | | `tests/wrapper/fortran/build_from_source/test_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | direct wrapper/build route | build/compile/link orchestration | `wrapper-plan` | diff --git a/docs/stylesheets/site.css b/docs/stylesheets/site.css index e88637b3d..e8c3e62d2 100644 --- a/docs/stylesheets/site.css +++ b/docs/stylesheets/site.css @@ -36,6 +36,128 @@ display: none; } +.x2py-repository-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + min-height: 2.15rem; + padding: 0.4rem 0.8rem; + border: 1px solid #24292f; + border-radius: 0.35rem; + background: #24292f; + box-shadow: 0 1px 2px rgb(0 0 0 / 18%); + color: #fff; + font-size: 0.82rem; + font-weight: 600; + line-height: 1; + text-decoration: none; + transition: + background-color 120ms ease, + border-color 120ms ease, + box-shadow 120ms ease, + transform 120ms ease; +} + +.x2py-repository-link .fa-github { + font-size: 1.15rem; +} + +.x2py-repository-link:visited { + color: #fff; +} + +.x2py-repository-link:hover, +.x2py-repository-link:focus { + border-color: #000; + background: #000; + box-shadow: 0 3px 7px rgb(0 0 0 / 22%); + color: #fff; + transform: translateY(-1px); +} + +.x2py-repository-link:focus-visible, +.x2py-page-source a:focus-visible { + outline: 2px solid #f5b041; + outline-offset: 2px; +} + +.x2py-page-source { + display: flex; + clear: both; + justify-content: flex-end; + padding-top: 2rem; +} + +.x2py-page-source a { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.3rem 0.1rem; + border-bottom: 1px solid transparent; + color: #606f7b; + font-size: 0.82rem; + font-weight: 600; + text-decoration: none; + transition: + border-color 120ms ease, + color 120ms ease; +} + +.x2py-page-source a:visited { + color: #606f7b; +} + +.x2py-page-source a:hover, +.x2py-page-source a:focus { + border-color: #2980b9; + color: #1f618d; +} + +.x2py-primary-cta { + display: inline-flex; + align-items: center; + min-height: 2.6rem; + padding: 0.65rem 1rem; + border: 1px solid #176b64; + border-radius: 0.35rem; + background: #176b64; + box-shadow: 0 2px 4px rgb(0 0 0 / 18%); + color: #fff; + font-weight: 700; + text-decoration: none; + transition: + background-color 120ms ease, + border-color 120ms ease, + box-shadow 120ms ease, + transform 120ms ease; +} + +.x2py-primary-cta:visited { + color: #fff; +} + +.x2py-primary-cta:hover, +.x2py-primary-cta:focus { + border-color: #0f514c; + background: #0f514c; + box-shadow: 0 4px 9px rgb(0 0 0 / 22%); + color: #fff; + transform: translateY(-1px); +} + +.x2py-primary-cta:focus-visible { + outline: 2px solid #f5b041; + outline-offset: 2px; +} + +@media screen and (max-width: 768px) { + .wy-breadcrumbs-aside { + display: block; + float: none; + margin-top: 0.75rem; + } +} + .rst-content pre { width: 100%; max-width: 56rem; diff --git a/docs/user/getting-started/beginner-workflow.md b/docs/user/getting-started/beginner-workflow.md index 6002d167c..97a8eb2e4 100644 --- a/docs/user/getting-started/beginner-workflow.md +++ b/docs/user/getting-started/beginner-workflow.md @@ -17,7 +17,8 @@ project: edit the source, review its Python interface, build, and test. ## Recommended Project Layout -This layout continues with the `scale.f90` example: +This layout continues with `scale.f90` from +[First Wrapped Function](first-wrapped-function.md): ``` my-project/ diff --git a/docs/user/getting-started/first-wrapped-function.md b/docs/user/getting-started/first-wrapped-function.md index 5de39674b..6e522c199 100644 --- a/docs/user/getting-started/first-wrapped-function.md +++ b/docs/user/getting-started/first-wrapped-function.md @@ -16,7 +16,7 @@ This example shows how to build a simple scalar Fortran function and call it fro ## Source Code -Use the same `scale.f90` from the homepage: +Create `scale.f90`: ```fortran real(8) function scale(value, factor) result(output) diff --git a/docs/user/getting-started/index.md b/docs/user/getting-started/index.md index bf513e6d2..9c9ea47e3 100644 --- a/docs/user/getting-started/index.md +++ b/docs/user/getting-started/index.md @@ -12,7 +12,8 @@ publication: reviewed This guide takes you from a fresh clone to your first working Python extension built from Fortran code. -The recommended beginner path uses the **GNU toolchain**, which offers the best compatibility right now. +The recommended beginner path uses the **GNU toolchain**, including +`gfortran`, which offers the best compatibility right now. --- @@ -30,7 +31,9 @@ Follow these pages in order: ## What You Will Build -By the end of this section you will be able to write Fortran and call it cleanly from Python: +In [Your First Function](first-wrapped-function.md), you will create +`scale.f90`, build it as a Python extension named `scale`, and call its +`scale` function: ```python import numpy as np @@ -41,11 +44,12 @@ result = scale.scale(np.float64(3.0), np.float64(2.5)) print(result) # 7.5 ``` -The first example exposes a standalone Fortran function directly on the extension. -Later examples show how Fortran modules become Python namespaces. +The first example exposes a standalone Fortran function directly on the extension. +Later guides show how Fortran modules become Python namespaces and derived +types become Python classes. --- ## Next -- Start with [Installation](installation.md). +**Ready? Start with [Installation](installation.md).** diff --git a/docs/user/guide/building-shared-library.md b/docs/user/guide/building-shared-library.md index a7d065646..5fd3a6c3a 100644 --- a/docs/user/guide/building-shared-library.md +++ b/docs/user/guide/building-shared-library.md @@ -13,6 +13,9 @@ publication: reviewed x2py turns Fortran source into a Python extension module. The final module is a native shared library that Python imports directly. +This page continues with `scale.f90` from the +[Common Beginner Workflow](../getting-started/beginner-workflow.md). + ## Build Run x2py on the source file and choose a build directory: diff --git a/docs/user/reference/cli-commands.md b/docs/user/reference/cli-commands.md index 56179047e..9c6a343f2 100644 --- a/docs/user/reference/cli-commands.md +++ b/docs/user/reference/cli-commands.md @@ -81,8 +81,8 @@ not mislabel them as preprocessing-only options. It also keeps short examples for a basic source build, an explicitly named extension, and semantic contract generation; `--help-build` labels its basic build, semantic-contract build, and manifest-replay examples separately. Both help levels reuse the canonical -`scale.f90` source and exact commands from the -[homepage example](../../index.md#try-x2py), which contains the +`points.f90` source and naming from the +[homepage example](../../index.md#see-it-in-action), which contains the complete source, basic build, import flow, and expected result. The full build help uses the following two forms: @@ -141,8 +141,8 @@ them: python3 -m x2py parse INPUT [INPUT ...] [OPTIONS] python3 -m x2py semantics INPUT [INPUT ...] [OPTIONS] -python3 -m x2py parse scale.f90 -python3 -m x2py semantics scale.f90 +python3 -m x2py parse points.f90 +python3 -m x2py semantics points.f90 ``` Parse-report controls such as `--show-vars` and `--print-limit` appear only in @@ -177,13 +177,13 @@ python3 -m x2py generate (--sources | --makefile) | `--makefile` | Writes wrapper sources, the replay manifest when applicable, and `Makefile.x2py` without compiling. | ```bash -python3 -m x2py generate --pyi scale.f90 --out contracts -python3 -m x2py generate --sources scale.f90 --out-dir build -python3 -m x2py generate --makefile scale.f90 --out-dir build +python3 -m x2py generate --pyi points.f90 --out contracts +python3 -m x2py generate --sources points.f90 --out-dir build +python3 -m x2py generate --makefile points.f90 --out-dir build ``` -These examples reuse `scale.f90` from the -[homepage example](../../index.md#try-x2py). +These examples reuse `points.f90` from the +[homepage example](../../index.md#see-it-in-action). These modes are mutually exclusive. Source and Makefile generation still run the preprocessing and semantic-policy stages needed to produce a valid wrapper diff --git a/docs_theme/breadcrumbs.html b/docs_theme/breadcrumbs.html new file mode 100644 index 000000000..51182ffd4 --- /dev/null +++ b/docs_theme/breadcrumbs.html @@ -0,0 +1,40 @@ +
+
    +
  • + {%- if page %} + {%- for doc in page.ancestors[::-1] %} + {%- if doc.url %} + + {%- else %} + + {%- endif %} + {%- endfor %} + + {%- endif %} +
  • + + + Source Code + +
  • +
+ {%- if config.theme.prev_next_buttons_location|lower in ['top', 'both'] + and page and (page.next_page or page.previous_page) %} + + {%- endif %} +
+
diff --git a/docs_theme/footer.html b/docs_theme/footer.html new file mode 100644 index 000000000..235f6226c --- /dev/null +++ b/docs_theme/footer.html @@ -0,0 +1,40 @@ +
+ {%- block next_prev %} + {%- if config.theme.prev_next_buttons_location|lower in ['bottom', 'both'] + and page and (page.next_page or page.previous_page) %} + + {%- endif %} + {%- endblock %} + + {%- if page and page.edit_url %} + + {%- endif %} + +
+ +
+ {%- if config.copyright %} +

{{ config.copyright }}

+ {%- endif %} +
+ + {% trans mkdocs_link='MkDocs', sphinx_link='{}'.format(gettext('theme')), rtd_link='Read the Docs' %}Built with %(mkdocs_link)s using a %(sphinx_link)s provided by %(rtd_link)s.{% endtrans %} +
diff --git a/mkdocs.yml b/mkdocs.yml index 2e79eea16..737097891 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,10 +2,11 @@ site_name: x2py site_url: https://pynumlab.github.io/x2py/ repo_url: https://github.com/PyNumLab/x2py repo_name: GitHub -edit_uri: edit/main/docs/ +edit_uri: blob/main/docs/ docs_dir: docs theme: name: readthedocs + custom_dir: docs_theme collapse_navigation: false include_homepage_in_sidebar: true navigation_depth: 4 diff --git a/tests/cli/test_argument_contract.py b/tests/cli/test_argument_contract.py index 5f371a539..b9583d2eb 100644 --- a/tests/cli/test_argument_contract.py +++ b/tests/cli/test_argument_contract.py @@ -523,10 +523,17 @@ def assert_group_order(help_text, *headings): assert "Basic wrapper build:" in top_help assert "Name the Python extension:" in top_help assert "Generate an editable semantic contract:" in top_help - assert "python3 -m x2py scale.f90" in top_help - assert "python3 -m x2py scale.f90 --out SCALE" in top_help - assert "python3 -m x2py generate --pyi scale.f90 --out contracts" in top_help - assert 'See README.md "Quick Start" for the scale.f90 source and expected output.' in top_help + assert "python3 -m x2py points.f90" in top_help + assert "python3 -m x2py points.f90 --out geometry" in top_help + assert "python3 -m x2py generate --pyi points.f90 --out contracts" in top_help + assert "See the x2py homepage for the points.f90 source and generated Python API:" in top_help + assert "https://pynumlab.github.io/x2py/#see-it-in-action" in top_help + assert "points.f90" in build_help + assert "points.f90" in parse_help + assert "points.f90" in semantics_help + assert "points.f90" in generate_help + for help_text in (top_help, build_help, parse_help, semantics_help, generate_help, probe_help): + assert "scale.f90" not in help_text assert "Run `python3 -m x2py --help-build` for the full list of build options." in top_help for command in ("parse", "semantics", "generate", "probe"): assert f"python3 -m x2py {command} --help" in top_help diff --git a/tests/cli/test_output_contract.py b/tests/cli/test_output_contract.py index 28443c4fa..092879f50 100644 --- a/tests/cli/test_output_contract.py +++ b/tests/cli/test_output_contract.py @@ -634,10 +634,11 @@ def test_cli_help_is_concise_and_points_to_detailed_help(): assert "Basic wrapper build:" in res.stdout assert "Name the Python extension:" in res.stdout assert "Generate an editable semantic contract:" in res.stdout - assert "python3 -m x2py scale.f90" in res.stdout - assert "python3 -m x2py scale.f90 --out SCALE" in res.stdout - assert "python3 -m x2py generate --pyi scale.f90 --out contracts" in res.stdout - assert 'See README.md "Quick Start" for the scale.f90 source and expected output.' in res.stdout + assert "python3 -m x2py points.f90" in res.stdout + assert "python3 -m x2py points.f90 --out geometry" in res.stdout + assert "python3 -m x2py generate --pyi points.f90 --out contracts" in res.stdout + assert "See the x2py homepage for the points.f90 source and generated Python API:" in res.stdout + assert "https://pynumlab.github.io/x2py/#see-it-in-action" in res.stdout assert "python3 -m x2py --help-build" in res.stdout assert "python3 -m x2py parse --help" in res.stdout assert "python3 -m x2py semantics --help" in res.stdout diff --git a/tests/data/fortran/wrapper/home_points.f90 b/tests/data/fortran/wrapper/home_points.f90 new file mode 100644 index 000000000..8ca97472b --- /dev/null +++ b/tests/data/fortran/wrapper/home_points.f90 @@ -0,0 +1,23 @@ +module points + implicit none + + type :: point + real(8) :: x = 0.0d0 + real(8) :: y = 0.0d0 + end type point + +contains + + subroutine move(item, dx, dy) + type(point), intent(inout) :: item + real(8), intent(in) :: dx, dy + item%x = item%x + dx + item%y = item%y + dy + end subroutine move + + real(8) function norm_squared(item) result(value) + type(point), intent(in) :: item + value = item%x * item%x + item%y * item%y + end function norm_squared + +end module points diff --git a/tests/docs/test_structure.py b/tests/docs/test_structure.py index 6f0add66b..cb71e2f1c 100644 --- a/tests/docs/test_structure.py +++ b/tests/docs/test_structure.py @@ -661,129 +661,89 @@ def test_deferred_c_pages_are_not_in_site_navigation() -> None: assert any("X2PY_C_DOCS" in line and "c-parser-reference.md" in line for line in lines) -def test_readme_quick_start_shows_input_source_before_wrapper_build() -> None: +def test_readme_follows_one_points_workflow_from_build_through_contract_rebuild() -> None: readme = _visible_documentation_source(ROOT / "README.md") quick_start = readme.split("## Installation & Quick Start", maxsplit=1)[1].split( - "The runtime wrapper mechanism is:", + "## How it works", maxsplit=1, )[0] - top_help = subprocess.run( - [sys.executable, "-m", "x2py", "--help"], - cwd=ROOT, - capture_output=True, - text=True, - check=True, - ).stdout - - assert "Basic wrapper build:" in top_help - for command in ( - "python3 -m x2py scale.f90", - "python3 -m x2py scale.f90 --out SCALE", - "python3 -m x2py generate --pyi scale.f90 --out contracts", - ): - assert command in quick_start - assert command in top_help - + installation_index = quick_start.index("git clone https://github.com/PyNumLab/x2py.git") help_index = quick_start.index("python3 -m x2py --help") - fortran_block_index = quick_start.index("```fortran") source_build_command_index = quick_start.index( - "python3 -m x2py scale.f90", - fortran_block_index, - ) - default_source_build_tree_index = quick_start.index( - ".\n scale.f90\n scale.so\n __x2py__/", source_build_command_index - ) - named_source_build_command_index = quick_start.index( - "python3 -m x2py scale.f90 --out SCALE", - default_source_build_tree_index, + "python3 -m x2py points.f90 --out geometry", + help_index, ) source_build_tree_index = quick_start.index( - ".\n scale.f90\n SCALE.so\n __x2py__/", - named_source_build_command_index, + ".\n points.f90\n geometry.so\n __x2py__/", + source_build_command_index, ) explicit_source_build_command_index = quick_start.index( - "python3 -m x2py scale.f90 \\\n --out SCALE \\\n --out-dir build/SCALE", + "python3 -m x2py points.f90 \\\n --out geometry \\\n --out-dir build/geometry", source_build_tree_index, ) - explicit_source_build_tree_index = quick_start.index("build/SCALE/", explicit_source_build_command_index) + explicit_source_build_tree_index = quick_start.index( + "build/geometry/\n geometry..so", + explicit_source_build_command_index, + ) pyi_generation_command_index = quick_start.index( - "python3 -m x2py generate --pyi scale.f90", + "python3 -m x2py generate --pyi points.f90 --out contracts", explicit_source_build_tree_index, ) - pyi_contract_tree_index = quick_start.index("contracts/\n __init__.pyi", pyi_generation_command_index) - pyi_contract_body_index = quick_start.index( - "@external\n@native_call([Addr(Arg(0)), Addr(Arg(1))])\ndef scale(\n" - " value: Float64,\n factor: Float64\n) -> Float64: ...", - pyi_contract_tree_index, - ) - pyi_build_command_index = quick_start.index( - "python3 -m x2py contracts/__init__.pyi", - pyi_contract_body_index, - ) - native_source_argument_index = quick_start.index("--native-fortran-sources scale.f90", pyi_build_command_index) - output_name_index = quick_start.index("--out SCALE", native_source_argument_index) - pyi_build_tree_index = quick_start.index("build/SCALE_from_pyi/", output_name_index) - direct_import_index = quick_start.index("SCALE.scale(", pyi_build_tree_index) - package_entry_import_section_index = quick_start.index("The package-entry `.pyi` build", direct_import_index) - pyi_import_index = quick_start.index("SCALE.scale(", package_entry_import_section_index) - runtime_output_index = quick_start.index("7.5", pyi_import_index) - points_source_index = quick_start.index("For a small derived-type wrapper", runtime_output_index) - points_fortran_index = quick_start.index("module points", points_source_index) - points_pyi_command_index = quick_start.index( - "python3 -m x2py generate --pyi points.f90 --out contracts", - points_fortran_index, + pyi_contract_tree_index = quick_start.index( + "contracts/\n __init__.pyi\n points.pyi", + pyi_generation_command_index, ) - points_contract_index = quick_start.index( + pyi_contract_body_index = quick_start.index( "class point:\n" " def __init__(\n" " self,\n" " *,\n" - " x: Float64 = ...,\n" - " y: Float64 = ...", - points_pyi_command_index, + " x: Float64 = 0.0,\n" + " y: Float64 = 0.0", + pyi_contract_tree_index, + ) + move_contract_index = quick_start.index( + "@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))])\ndef move(", + pyi_contract_body_index, ) - points_norm_contract_index = quick_start.index("def norm_squared(", points_contract_index) - points_build_command_index = quick_start.index( - "python3 -m x2py points.f90 --out geometry --out-dir build/geometry", - points_norm_contract_index, + norm_contract_index = quick_start.index("def norm_squared(", move_contract_index) + pyi_build_command_index = quick_start.index( + "python3 -m x2py contracts/__init__.pyi", + norm_contract_index, ) - points_import_index = quick_start.index("import geometry", points_build_command_index) - points_norm_call_index = quick_start.index("geometry.points.norm_squared(p)", points_import_index) - points_output_index = quick_start.index("4.0 2.0\n20.0", points_norm_call_index) + native_source_argument_index = quick_start.index("--native-fortran-sources points.f90", pyi_build_command_index) + output_name_index = quick_start.index("--out geometry", native_source_argument_index) + pyi_build_tree_index = quick_start.index("build/geometry_from_pyi/", output_name_index) + import_index = quick_start.index("import geometry.points as points", pyi_build_tree_index) + constructor_index = quick_start.index("item = points.point(", import_index) + mutation_index = quick_start.index("points.move(item", constructor_index) + runtime_output_index = quick_start.index("# 20.0", mutation_index) verbose_command_index = quick_start.index( - "python3 -m x2py scale.f90 \\\n --out SCALE_debug", - points_output_index, + "python3 -m x2py points.f90 \\\n --out geometry_debug", + runtime_output_index, ) verbose_fortran_flag_index = quick_start.index("--wrapper-fortran-flags=-O2", verbose_command_index) verbose_c_flag_index = quick_start.index("--wrapper-c-flags=-O2", verbose_fortran_flag_index) verbose_output_index = quick_start.index("generated Python binding", verbose_c_flag_index) - module_lesson_index = quick_start.index("first wrapped module", verbose_output_index) - assert help_index < fortran_block_index < source_build_command_index - assert source_build_command_index < default_source_build_tree_index < named_source_build_command_index - assert named_source_build_command_index < source_build_tree_index < explicit_source_build_command_index + assert installation_index < help_index < source_build_command_index + assert source_build_command_index < source_build_tree_index < explicit_source_build_command_index assert explicit_source_build_command_index < explicit_source_build_tree_index < pyi_generation_command_index assert pyi_generation_command_index < pyi_contract_tree_index < pyi_contract_body_index - assert pyi_contract_body_index < pyi_build_command_index < native_source_argument_index < output_name_index - assert output_name_index < pyi_build_tree_index < direct_import_index < package_entry_import_section_index - assert package_entry_import_section_index < pyi_import_index - assert pyi_import_index < runtime_output_index < points_source_index - assert points_source_index < points_fortran_index < points_pyi_command_index < points_contract_index - assert points_contract_index < points_norm_contract_index < points_build_command_index < points_import_index - assert points_import_index < points_norm_call_index < points_output_index < verbose_command_index + assert pyi_contract_body_index < move_contract_index < norm_contract_index < pyi_build_command_index + assert pyi_build_command_index < native_source_argument_index < output_name_index < pyi_build_tree_index + assert pyi_build_tree_index < import_index < constructor_index < mutation_index < runtime_output_index + assert runtime_output_index < verbose_command_index assert verbose_command_index < verbose_fortran_flag_index < verbose_c_flag_index < verbose_output_index - assert verbose_output_index < module_lesson_index assert "--parse" not in readme assert "--semantics" not in readme - assert "tests/data/fortran/wrapper/scale.f90" in quick_start - assert "scale.f90 --json" not in quick_start + assert "scale.f90" not in readme + assert "SCALE" not in readme assert "python3 -m x2py solver.f90" not in quick_start - assert "python3 -m x2py tests/data/fortran/wrapper/scale.f90" not in quick_start assert "fruntime_abi_f90" not in readme assert "solver.f90" not in readme assert "add1" not in readme assert "distance2" not in readme - assert "points_api" not in readme assert "point_api" not in readme assert "build/points" not in readme assert "tests/data/fortran/general/basic_subroutine.f90" not in readme @@ -1012,31 +972,67 @@ def test_reviewed_user_pages_do_not_contain_editorial_notes(relative_path: str) def test_getting_started_overview_uses_standalone_example() -> None: overview = (DOCS_ROOT / "user/getting-started/index.md").read_text(encoding="utf-8") + introduction_index = overview.index("you will create\n`scale.f90`") + import_index = overview.index("import scale") + call_index = overview.index("scale.scale(np.float64(3.0), np.float64(2.5))") - assert "scale.scale(np.float64(3.0), np.float64(2.5))" in overview + assert introduction_index < import_index < call_index + assert "[Your First Function](first-wrapped-function.md)" in overview def test_documentation_homepage_demonstrates_x2py_before_getting_started() -> None: page = (DOCS_ROOT / "index.md").read_text(encoding="utf-8") - introduction_index = page.index("x2py turns supported Fortran source") - try_heading_index = page.index("## Try it in 30 seconds {#try-x2py}") - source_index = page.index("```fortran", try_heading_index) - build_index = page.index("python3 -m x2py scale.f90") - call_index = page.index("result = scale.scale(np.float64(3.0), np.float64(2.5))") - output_index = page.index("7.5", call_index) - docstring_index = page.index("scale(value, factor) -> float64", output_index) - getting_started_index = page.index("Getting Started](user/getting-started/index.md)") - - assert introduction_index < try_heading_index < source_index < build_index < call_index < output_index - assert output_index < docstring_index < getting_started_index - assert "value : float64\nfactor : float64" in page - assert "result : float64" in page - assert "If an argument has an incompatible Python type or dtype." in page + introduction_index = page.index("Turn Fortran into natural Python APIs") + build_index = page.index("python3 -m x2py points.f90 --out geometry") + example_heading_index = page.index("## See it in action") + source_index = page.index("```fortran", example_heading_index) + generated_api_index = page.index("**Generated Python API:**", source_index) + constructor_index = page.index("item = points.point(", generated_api_index) + mutation_index = page.index("points.move(item", constructor_index) + result_index = page.index("# 4.0 2.0", mutation_index) + contract_index = page.index("Edit the generated `.pyi` contract", result_index) + features_index = page.index("## Key Features", contract_index) + getting_started_index = page.index("Getting Started Guide →", features_index) + + assert introduction_index < build_index < example_heading_index < source_index + assert source_index < generated_api_index < constructor_index < mutation_index + assert mutation_index < result_index < contract_index < features_index < getting_started_index + assert "print(points.norm_squared(item))" in page + assert "# 20.0" in page + assert "[contract guide](user/reference/pyi-contracts/index.md)" in page + assert "{ .x2py-primary-cta }" in page assert "developer/index.md" not in page assert "maintainer/README.md" not in page assert "user/guide/" not in page +def test_readme_opening_uses_the_homepage_message_and_showcase() -> None: + homepage = _visible_documentation_source(DOCS_ROOT / "index.md") + readme = _visible_documentation_source(ROOT / "README.md") + readme_opening = readme.split("## Installation & Quick Start", maxsplit=1)[0] + shared_content = ( + "**Turn Fortran into natural Python APIs.**", + "Build clean, importable native extensions from supported Fortran without\nwriting low-level binding code.", + "python3 -m x2py points.f90 --out geometry", + "", + "import geometry.points as points", + "item = points.point(x=np.float64(3.0), y=np.float64(4.0))", + "points.move(item, np.float64(1.0), np.float64(-2.0))", + "print(points.norm_squared(item)) # 20.0", + "No manual bindings are required.", + "## Key Features", + "- Editable `.pyi` contracts and readable generated docstrings", + ) + + for content in shared_content: + assert content in homepage + assert content in readme_opening + + assert readme.count("\nmodule points\n") == 1 + assert readme_opening.count("python3 -m x2py points.f90 --out geometry") == 1 + assert readme_opening.count("import geometry.points as points") == 1 + + def test_documentation_links_to_documentation_stay_on_the_website() -> None: github_documentation_prefixes = ( "https://github.com/PyNumLab/x2py/blob/main/docs/", @@ -1133,6 +1129,7 @@ def test_beginner_workflow_reuses_scale_example_without_renaming_it() -> None: assert source_reference_index < layout_index < contract_index < build_index assert build_index < smoke_index < editing_index < edited_contract_index < diagnosis_index + assert "[First Wrapped Function](first-wrapped-function.md)" in page assert "scale_api" not in page @@ -1205,8 +1202,20 @@ def test_user_guide_shows_direct_shared_library_build() -> None: content = "\n".join( _visible_documentation_source(DOCS_ROOT / relative_path) for relative_path in REQUIRED_USER_GUIDE_PAGES ) + shared_library = _visible_documentation_source(DOCS_ROOT / "user/guide/building-shared-library.md") assert "python3 -m x2py src/scale.f90 --out-dir build/scale" in content + assert "[Common Beginner Workflow](../getting-started/beginner-workflow.md)" in shared_library + + +def test_cli_reference_reuses_the_homepage_points_example() -> None: + content = _visible_documentation_source(CLI_REFERENCE_PATH) + + assert "`points.f90` source and naming" in content + assert "../../index.md#see-it-in-action" in content + assert "python3 -m x2py parse points.f90" in content + assert "python3 -m x2py generate --pyi points.f90 --out contracts" in content + assert "scale.f90" not in content def test_fortran_wrapper_reference_shows_every_common_shared_library_build_input() -> None: diff --git a/tests/wrapper/fortran/build_from_source/test_build_modes.py b/tests/wrapper/fortran/build_from_source/test_build_modes.py index f1691593b..ddc60aa1e 100644 --- a/tests/wrapper/fortran/build_from_source/test_build_modes.py +++ b/tests/wrapper/fortran/build_from_source/test_build_modes.py @@ -19,6 +19,7 @@ DEFAULT_OUTPUT_SOURCE = wrapper_source("fdefault_output.f") SCALE_SOURCE = wrapper_source("scale.f90") SCALAR_SOURCE = wrapper_source("fmath.f") +HOME_POINTS_SOURCE = wrapper_source("home_points.f90") def test_verbose_mode_prints_full_direct_build_commands(tmp_path: Path): @@ -257,6 +258,48 @@ def test_fortran_wrapper_out_names_importable_shared_library(tmp_path: Path): assert module.scale(np.float64(3.0), np.float64(2.5)) == np.float64(7.5) +def test_documented_homepage_points_example_builds_and_imports(tmp_path: Path): + source = tmp_path / "points.f90" + build_dir = tmp_path / "build" / "geometry" + shutil.copyfile(HOME_POINTS_SOURCE, source) + + subprocess.run( + [ + sys.executable, + "-m", + "x2py", + str(source), + "--out", + "geometry", + "--out-dir", + str(build_dir), + ], + capture_output=True, + text=True, + check=True, + cwd=tmp_path, + ) + + assert (tmp_path / "geometry.so").is_file() + assert len(tuple(build_dir.glob("geometry.*.so"))) == 1 + + sys.modules.pop("geometry.points", None) + sys.modules.pop("geometry", None) + sys.path.insert(0, str(tmp_path)) + try: + geometry = importlib.import_module("geometry") + points = geometry.points + item = points.point(x=np.float64(3.0), y=np.float64(4.0)) + points.move(item, np.float64(1.0), np.float64(-2.0)) + assert item.x == np.float64(4.0) + assert item.y == np.float64(2.0) + assert points.norm_squared(item) == np.float64(20.0) + finally: + sys.path.remove(str(tmp_path)) + sys.modules.pop("geometry.points", None) + sys.modules.pop("geometry", None) + + def test_internal_preprocessing_mode_still_builds_importable_runtime_wrapper(tmp_path: Path): source = tmp_path / SCALAR_SOURCE.name build_dir = tmp_path / "build" diff --git a/x2py/cli.py b/x2py/cli.py index e77266235..ccaea5c57 100644 --- a/x2py/cli.py +++ b/x2py/cli.py @@ -60,6 +60,10 @@ " --build-manifest PATH [OVERRIDES]" ) _PROBE_USAGE = "%(prog)s --language {fortran,c} --compiler COMPILER [OPTIONS]" +_POINTS_EXAMPLE_HELP = ( + " See the x2py homepage for the points.f90 source and generated Python API:\n" + " https://pynumlab.github.io/x2py/#see-it-in-action\n" +) _CLI_HELP_DESCRIPTION = ( "Build Python extensions from Fortran and inspect native interface artifacts.\n\n" "commands:\n" @@ -71,13 +75,13 @@ _CLI_HELP_EPILOG = ( f"{_HELP_DIVIDER}\n\n" " Basic wrapper build:\n" - " python3 -m x2py scale.f90\n" + " python3 -m x2py points.f90\n" "\n" " Name the Python extension:\n" - " python3 -m x2py scale.f90 --out SCALE\n\n" + " python3 -m x2py points.f90 --out geometry\n\n" " Generate an editable semantic contract:\n" - " python3 -m x2py generate --pyi scale.f90 --out contracts\n\n" - ' See README.md "Quick Start" for the scale.f90 source and expected output.\n\n' + " python3 -m x2py generate --pyi points.f90 --out contracts\n\n" + f"{_POINTS_EXAMPLE_HELP}\n" " More help:\n" " python3 -m x2py --help-build\n" " python3 -m x2py parse --help\n" @@ -90,17 +94,17 @@ _BUILD_HELP_EPILOG = ( f"{_HELP_DIVIDER}\n\n" " Basic wrapper build:\n" - " python3 -m x2py scale.f90\n" + " python3 -m x2py points.f90\n" "\n" " Name the Python extension:\n" - " python3 -m x2py scale.f90 --out SCALE\n" + " python3 -m x2py points.f90 --out geometry\n" "\n" " Build from a semantic contract:\n" - " python3 -m x2py contracts/__init__.pyi --native-fortran-sources scale.f90 \\\n" - " --out SCALE --out-dir build/SCALE_from_pyi\n\n" + " python3 -m x2py contracts/__init__.pyi --native-fortran-sources points.f90 \\\n" + " --out geometry --out-dir build/geometry_from_pyi\n\n" " Replay a build manifest:\n" " python3 -m x2py --build-manifest build/x2py-build.json\n\n" - ' See README.md "Quick Start" for the scale.f90 source and expected output.\n' + f"{_POINTS_EXAMPLE_HELP}" " See docs/user/reference/cli-commands.md for all build options.\n\n" " Manifest overrides: --out, --compiler, -I/--include-dir, --json, --verbose,\n" " --no-color, and --debug." @@ -108,38 +112,38 @@ _PARSE_HELP_EPILOG = ( f"{_HELP_DIVIDER}\n\n" " Basic Fortran inspection:\n" - " python3 -m x2py parse scale.f90\n" + " python3 -m x2py parse points.f90\n" "\n" " Detailed Fortran report:\n" - " python3 -m x2py parse scale.f90 --show-vars --print-limit 50\n" + " python3 -m x2py parse points.f90 --show-vars --print-limit 50\n" "\n" " C header as JSON:\n" " python3 -m x2py parse path/to/api.h --language c --json\n\n" - ' See README.md "Quick Start" for the scale.f90 source.' + f"{_POINTS_EXAMPLE_HELP}" ) _SEMANTICS_HELP_EPILOG = ( f"{_HELP_DIVIDER}\n\n" " Basic Fortran conversion:\n" - " python3 -m x2py semantics scale.f90\n" + " python3 -m x2py semantics points.f90\n" "\n" " C header:\n" " python3 -m x2py semantics path/to/api.h --language c\n" "\n" " Save semantic IR:\n" - " python3 -m x2py semantics scale.f90 --out semantics.json\n\n" - ' See README.md "Quick Start" for the scale.f90 source.' + " python3 -m x2py semantics points.f90 --out semantics.json\n\n" + f"{_POINTS_EXAMPLE_HELP}" ) _GENERATE_HELP_EPILOG = ( f"{_HELP_DIVIDER}\n\n" " Editable semantic contract:\n" - " python3 -m x2py generate --pyi scale.f90 --out contracts\n" + " python3 -m x2py generate --pyi points.f90 --out contracts\n" "\n" " Wrapper sources only:\n" - " python3 -m x2py generate --sources scale.f90 --out-dir build\n" + " python3 -m x2py generate --sources points.f90 --out-dir build\n" "\n" " Reproducible Makefile build:\n" - " python3 -m x2py generate --makefile scale.f90 --out-dir build\n\n" - ' See README.md "Quick Start" for the scale.f90 source.' + " python3 -m x2py generate --makefile points.f90 --out-dir build\n\n" + f"{_POINTS_EXAMPLE_HELP}" ) _PROBE_HELP_EPILOG = ( f"{_HELP_DIVIDER}\n\n"