diff --git a/.github/workflows/cff-validator.yml b/.github/workflows/cff-validator.yml index 6fce818..598eaed 100644 --- a/.github/workflows/cff-validator.yml +++ b/.github/workflows/cff-validator.yml @@ -2,10 +2,12 @@ name: Validate CITATION.cff on: push: + branches: ["main", "dev"] paths: - 'CITATION.cff' - '.github/workflows/cff-validator.yml' pull_request: + branches: ["main", "dev"] paths: - 'CITATION.cff' - '.github/workflows/cff-validator.yml' @@ -22,6 +24,4 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Validate CITATION.cff - uses: dieghernan/cff-validator@v3 - with: - install-r: true + uses: dieghernan/cff-validator@v4 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..dc73c56 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,93 @@ +name: Test with pytest + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - main + - dev + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + LIBRARY_BASE_REPO: NTIA/LFMF + LIBRARY_RELEASE_TAG: v1.1-rc.2 + LIBRARY_DESTINATION_DIRECTORY: 'src/ITS/Propagation/LFMF' + +jobs: + run-all-tests: + name: ${{ matrix.platform.os-name }} / Py${{ matrix.py }} + runs-on: ${{ matrix.platform.os-runner }} + strategy: + fail-fast: false + matrix: + platform: + - os-name: 'Windows (64-bit)' + os-runner: 'windows-latest' + arch-id: 'x64' + release-file-pattern: '*-x64.dll' + - os-name: 'Windows (32-bit)' + os-runner: 'windows-latest' + arch-id: 'x86' + release-file-pattern: '*-x86.dll' + - os-name: 'macOS (intel/x64)' + os-runner: 'macos-13' + arch-id: 'x64' + release-file-pattern: '*.dylib' + - os-name: 'macOS (apple/arm64)' + os-runner: 'macos-latest' + arch-id: 'arm64' + release-file-pattern: '*.dylib' + - os-name: 'Linux (Ubuntu)' + os-runner: 'ubuntu-latest' + arch-id: 'x64' + release-file-pattern: '*.so' + py: # Python versions to test on all platforms + - "3.9" + - "3.10" + - "3.11" + - "3.12" + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + submodules: true + + # Cache key is unique to the combination of runner OS + architecture (matrix.arch-id) + release tag + - name: Restore ${{ env.LIBRARY_RELEASE_TAG }} binaries from cache if available + id: cache-restore + uses: actions/cache@v4 + with: + key: ${{ runner.os }}-${{ matrix.platform.arch-id }}-${{ env.LIBRARY_RELEASE_TAG }} + path: ${{ env.LIBRARY_DESTINATION_DIRECTORY}}/${{ matrix.platform.release-file-pattern }} + + # Only the binaries required for the current platform are downloaded. Note that the distributed + # wheel for proplib python packages includes all binaries, so that the wheel is inherently cross-platform. + - name: Download required ${{ env.LIBRARY_RELEASE_TAG }} binaries + if: ${{ steps.cache-restore.outputs.cache-hit != 'true' }} + uses: robinraju/release-downloader@v1 + with: + repository: ${{ env.LIBRARY_BASE_REPO }} + tag: ${{ env.LIBRARY_RELEASE_TAG }} + fileName: ${{ matrix.platform.release-file-pattern }} + tarBall: false + zipBall: false + out-file-path: ${{ env.LIBRARY_DESTINATION_DIRECTORY }} + + - name: Set up Python ${{ matrix.py }} + uses: actions/setup-python@v5 + with: + architecture: ${{ matrix.platform.arch-id }} + python-version: ${{ matrix.py }} + cache: 'pip' + + - name: Install dependencies for testing + run: python -m pip install -e .[tests] + + - name: Run pytest + run: pytest --cov-report=term-missing --no-cov-on-fail --cov diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..71aeace --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +# Action builds a universal (Win32/Win64/macOS-universal/Linux-x64) Python wheel +# from the source code, using Hatchling, and uploads it as an artifact. An sdist (.tar.gz) is +# also uploaded, which includes all platform shared library files. These artifacts should be +# used when creating new releases on PyPI and GitHub. The action is triggered by pushes into `main` +# or pull_requests into `main` or `dev` (for testing). To aid in releases, the workflow is +# also triggered when new SemVer tags are created. +name: Build Release Artifacts + +on: + workflow_dispatch: + push: + branches: + - main + tags: + - 'v[0-9]+.*' + pull_request: + branches: + - main + - dev + +env: + LIBRARY_BASE_REPO: NTIA/LFMF + LIBRARY_RELEASE_TAG: v1.1-rc.2 + LIBRARY_DESTINATION_DIRECTORY: 'src/ITS/Propagation/LFMF/' + +jobs: + build_wheel: + name: Build a universal, cross-platform wheel + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + submodules: true + + # Only the binaries required for the current platform are downloaded. Note that the distributed + # wheel for proplib python packages includes all binaries, so that the wheel is inherently cross-platform. + - name: Download required ${{ env.LIBRARY_RELEASE_TAG }} Windows binaries + uses: robinraju/release-downloader@v1 + with: + repository: ${{ env.LIBRARY_BASE_REPO }} + tag: ${{ env.LIBRARY_RELEASE_TAG }} + fileName: '*.dll' + tarBall: false + zipBall: false + out-file-path: ${{ env.LIBRARY_DESTINATION_DIRECTORY }} + + - name: Download required ${{ env.LIBRARY_RELEASE_TAG }} Linux binaries + uses: robinraju/release-downloader@v1 + with: + repository: ${{ env.LIBRARY_BASE_REPO }} + tag: ${{ env.LIBRARY_RELEASE_TAG }} + fileName: '*.so' + tarBall: false + zipBall: false + out-file-path: ${{ env.LIBRARY_DESTINATION_DIRECTORY }} + + - name: Download required ${{ env.LIBRARY_RELEASE_TAG }} macOS binaries + uses: robinraju/release-downloader@v1 + with: + repository: ${{ env.LIBRARY_BASE_REPO }} + tag: ${{ env.LIBRARY_RELEASE_TAG }} + fileName: '*.dylib' + tarBall: false + zipBall: false + out-file-path: ${{ env.LIBRARY_DESTINATION_DIRECTORY }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install build dependencies + run: pip install hatchling + + - name: Build wheels + run: hatchling build + + - uses: actions/upload-artifact@v4 + with: + name: Release Artifacts (sdist and wheel) + path: dist/* diff --git a/.gitignore b/.gitignore index ac9216b..611c769 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +.idea/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c05fabe --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/data"] + path = tests/data + url = https://github.com/NTIA/LFMF-test-data diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7ec2e1..5d06997 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: ["--py39-plus"] diff --git a/.zenodo.json b/.zenodo.json index 67993b1..690a57a 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -1,6 +1,6 @@ { "upload_type": "software", - "publication_date": "TODO-TEMPLATE", + "publication_date": "2025-1-22", "title": "Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python Wrapper", "creators": [ { @@ -15,7 +15,6 @@ } ], "description": "This code repository contains a Python wrapper for the NTIA/ITS implementation of the Low Frequency / Medium Frequency (LF/MF) Propagation Model.", - "access_right": "open", "keywords": [ "propagation", "communications", @@ -36,7 +35,7 @@ { "identifier": "https://ntia.github.io/propagation-library-wiki/models/LFMF/", "relation": "isDocumentedBy", - "resource_type": "softwaredocumentation" + "resource_type": "publication-softwaredocumentation" } ], "version": "1.1.0" diff --git a/README.md b/README.md index 8483437..3e1e8f3 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python® Wrapper # [![NTIA/ITS PropLib][proplib-badge]][proplib-link] -[![GitHub Issues][gh-issues-badge]][gh-issues-link] [![PyPI Release][pypi-release-badge]][pypi-release-link] [![GitHub Actions Unit Test Status][gh-actions-test-badge]][gh-actions-test-link] +[![GitHub Issues][gh-issues-badge]][gh-issues-link] [![DOI][doi-badge]][doi-link] [proplib-badge]: https://img.shields.io/badge/PropLib-badge?label=%F0%9F%87%BA%F0%9F%87%B8%20NTIA%2FITS&labelColor=162E51&color=D63E04 [proplib-link]: https://ntia.github.io/propagation-library-wiki -[gh-actions-test-badge]: https://img.shields.io/github/actions/workflow/status/NTIA/LFMF-python/tox.yml?branch=main&logo=pytest&logoColor=ffffff&label=Tests&labelColor=162E51 -[gh-actions-test-link]: https://github.com/NTIA/LFMF-python/actions/workflows/tox.yml +[gh-actions-test-badge]: https://img.shields.io/github/actions/workflow/status/NTIA/LFMF-python/pytest.yml?branch=main&logo=pytest&logoColor=ffffff&label=Tests&labelColor=162E51 +[gh-actions-test-link]: https://github.com/NTIA/LFMF-python/actions/workflows/pytest.yml [pypi-release-badge]: https://img.shields.io/pypi/v/proplib-lfmf?logo=pypi&logoColor=ffffff&label=Release&labelColor=162E51&color=D63E04 [pypi-release-link]: https://pypi.org/project/proplib-lfmf [gh-issues-badge]: https://img.shields.io/github/issues/NTIA/LFMF-python?logo=github&label=Issues&labelColor=162E51 diff --git a/pyproject.toml b/pyproject.toml index 7ffe4d1..24569ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "LFMF" +name = "proplib-lfmf" dynamic = ["version"] description = "A Python wrapper for the NTIA/ITS implementation of the Low Frequency / Medium Frequency (LF/MF) Propagation Model" readme = "README.md" @@ -42,7 +42,6 @@ classifiers = [ tests = [ "pytest>=8.2.0,<9.0", "pytest-cov>=6.0.0,<7.0", - "tox>=4.21.1,<5.0", # Keep in sync with tool.tox.min_version ] dev = [ "hatchling>=1.25.0,<2.0", @@ -51,6 +50,7 @@ dev = [ ] [project.urls] +"PropLib Wiki" = "https://ntia.github.io/propagation-library-wiki/models/LFMF/" "Python Wrapper Source" = "https://github.com/NTIA/LFMF-python" "Python Wrapper Bug Tracker" = "https://github.com/NTIA/LFMF-python/issues" "C++ Source" = "https://github.com/NTIA/LFMF" @@ -64,29 +64,9 @@ path = "src/ITS/Propagation/LFMF/__init__.py" packages = ["src/ITS"] ignore-vcs = true +[tool.hatch.build.targets.sdist] +ignore-vcs = true + [tool.cibuildwheel] test-command = "pytest ." test-requires = "pytest" - -[tool.tox] -min_version = "4.21.1" -env_list = ["3.9", "3.10", "3.11", "3.12", "3.13"] -skip_missing_interpreters = true - -[tool.tox.env.testenv] -description = "Run tests with pytest and generate coverage report" -extras = "tests" -commands = [ - "pytest", - "--cov-report=term-missing", - "--no-cov-on-fail", - "--cov", - { replace = "posargs", extend = true }, -] - -[tool.tox.gh.python] # tox-gh config for GitHub Actions testing -"3.9" = ["3.9"] -"3.10" = ["3.10"] -"3.11" = ["3.11"] -"3.12" = ["3.12"] -"3.13" = ["3.13"] diff --git a/src/ITS/Propagation/LFMF/proplib_loader.py b/src/ITS/Propagation/LFMF/proplib_loader.py index ff8312d..21702e3 100644 --- a/src/ITS/Propagation/LFMF/proplib_loader.py +++ b/src/ITS/Propagation/LFMF/proplib_loader.py @@ -43,6 +43,7 @@ """ import platform +import struct from ctypes import * from pathlib import Path @@ -67,15 +68,24 @@ def get_lib_name(lib_name: str) -> str: :param lib_name: The library name, with no extension or path, e.g., "P2108-1.0" :raises NotImplementedError: For platforms other than Windows, Linux, or macOS. + :raises RuntimeError: On Windows, if unable to determine system architecture. :return: The full filename, including path and extension, of the library. """ # Load the compiled library if platform.uname()[0] == "Windows": - lib_name += ".dll" + arch = struct.calcsize("P") * 8 # 32 or 64 + if arch == 64: + lib_name += "-x64.dll" + elif arch == 32: + lib_name += "-x86.dll" + else: + raise RuntimeError( + "Failed to determine system architecture for DLL loading" + ) elif platform.uname()[0] == "Linux": - lib_name += ".so" + lib_name += "-x86_64.so" elif platform.uname()[0] == "Darwin": - lib_name += ".dylib" + lib_name += "-universal.dylib" else: raise NotImplementedError("Your OS is not yet supported") # Library should be in the same directory as this file diff --git a/tests/data b/tests/data new file mode 160000 index 0000000..d3cc4d6 --- /dev/null +++ b/tests/data @@ -0,0 +1 @@ +Subproject commit d3cc4d6efff973b1a4fac4dbf3074439aa3246ba diff --git a/tests/test_utils.py b/tests/test_utils.py index b23066e..a4b7633 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,7 +7,6 @@ # Check if test data directory exists and is not empty if not TEST_DATA_DIR.exists() or not any(TEST_DATA_DIR.iterdir()): - _test_data_checked = True raise RuntimeError( f"Test data is not available in {TEST_DATA_DIR}.\n Try running " + "`git submodule init` and `git submodule update` to clone the test data submodule."