Skip to content

Commit c3e3fae

Browse files
Initial PropLib Python wrapper template
1 parent 6bab9e7 commit c3e3fae

10 files changed

+673
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ __pycache__/
55

66
# C extensions
77
*.so
8+
*.so.*
9+
*.dylib
10+
*.DLL
11+
*.dll
812

913
# Distribution / packaging
1014
.Python

.pre-commit-config.yaml

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
default_language_version:
2+
python: python3.9
3+
repos:
4+
- repo: https://github.com/pre-commit/pre-commit-hooks
5+
rev: v5.0.0
6+
hooks:
7+
- id: check-ast
8+
types: [file, python]
9+
- id: check-case-conflict
10+
- id: check-docstring-first
11+
types: [file, python]
12+
- id: check-merge-conflict
13+
- id: check-yaml
14+
types: [file, yaml]
15+
- id: debug-statements
16+
types: [file, python]
17+
- id: detect-private-key
18+
- id: end-of-file-fixer
19+
- id: trailing-whitespace
20+
- repo: https://github.com/asottile/pyupgrade
21+
rev: v3.19.0
22+
hooks:
23+
- id: pyupgrade
24+
args: ["--py39-plus"]
25+
- repo: https://github.com/pycqa/isort
26+
rev: 5.13.2
27+
hooks:
28+
- id: isort
29+
name: isort (python)
30+
types: [file, python]
31+
args: ["--profile", "black", "--filter-files", "--gitignore"]
32+
- repo: https://github.com/psf/black
33+
rev: 24.10.0
34+
hooks:
35+
- id: black
36+
types: [file, python]
37+
- repo: https://github.com/igorshubovych/markdownlint-cli
38+
rev: v0.42.0
39+
hooks:
40+
- id: markdownlint
41+
types: [file, markdown]
42+
exclude: GitHubRepoPublicReleaseApproval.md
43+
args: ["--disable", "MD013"]

CONTRIBUTING.md

+350
Large diffs are not rendered by default.

LICENSE.md

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# SOFTWARE DISCLAIMER / RELEASE
2+
3+
This software was developed by employees of the National Telecommunications and Information
4+
Administration (NTIA), an agency of the Federal Government and is provided to you
5+
as a public service. Pursuant to Title 15 United States Code Section 105, works
6+
of NTIA employees are not subject to copyright protection within the United States.
7+
8+
The software is provided by NTIA “AS IS.” NTIA MAKES NO WARRANTY OF ANY KIND, EXPRESS,
9+
IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY,
10+
FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NTIA does
11+
not warrant or make any representations regarding the use of the software or the
12+
results thereof, including but not limited to the correctness, accuracy, reliability
13+
or usefulness of the software.
14+
15+
To the extent that NTIA holds rights in countries other than the United States,
16+
you are hereby granted the non-exclusive irrevocable and unconditional right to
17+
print, publish, prepare derivative works and distribute the NTIA software, in any
18+
medium, or authorize others to do so on your behalf, on a royalty-free basis throughout
19+
the World.
20+
21+
You may improve, modify, and create derivative works of the software or any portion
22+
of the software, and you may copy and distribute such modifications or works. Modified
23+
works should carry a notice stating that you changed the software and should note
24+
the date and nature of any such change.
25+
26+
You are solely responsible for determining the appropriateness of using and distributing
27+
the software and you assume all risks associated with its use, including but not
28+
limited to the risks and costs of program errors, compliance with applicable laws,
29+
damage to or loss of data, programs or equipment, and the unavailability or interruption
30+
of operation. This software is not intended to be used in any situation where a failure
31+
could cause risk of injury or damage to property.
32+
33+
Please provide appropriate acknowledgments of NTIA’s creation of the software in
34+
any copies or derivative works of this software.

pyproject.toml

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "TODO-TEMPLATE"
7+
dynamic = ["version"]
8+
description = "TODO-TEMPLATE"
9+
readme = "README.md"
10+
requires-python = ">=3.9"
11+
license = { file = "LICENSE.md" }
12+
13+
authors = [
14+
{ name = "The Institute for Telecommunication Sciences", email = "[email protected]" },
15+
]
16+
17+
keywords = ["TODO-TEMPLATE", "NTIA", "ITS"]
18+
19+
classifiers = [
20+
"Intended Audience :: Science/Research",
21+
"Intended Audience :: Telecommunications Industry",
22+
"License :: Public Domain",
23+
"Natural Language :: English",
24+
"Operating System :: MacOS",
25+
"Operating System :: Microsoft :: Windows",
26+
"Operating System :: POSIX :: Linux",
27+
"Programming Language :: Python",
28+
"Programming Language :: Python :: 3",
29+
"Programming Language :: Python :: 3.9",
30+
"Programming Language :: Python :: 3.10",
31+
"Programming Language :: Python :: 3.11",
32+
"Programming Language :: Python :: 3.12",
33+
"Programming Language :: Python :: 3.13",
34+
]
35+
36+
[project.optional-dependencies]
37+
tests = [
38+
"pytest>=8.2.0,<9.0",
39+
"pytest-cov>=6.0.0,<7.0",
40+
"tox>=4.23.2,<5.0", # Keep in sync with tox.ini minimum_version
41+
]
42+
dev = [
43+
"hatchling>=1.5.0,<2.0",
44+
"pre-commit>=4.0.1,<5.0",
45+
"proplib_template[tests]", # TODO-TEMPLATE set to this package
46+
]
47+
48+
[project.urls]
49+
"Python Wrapper Source" = "https://github.com/NTIA/TODO-TEMPLATE"
50+
"Python Wrapper Bug Tracker" = "https://github.com/NTIA/TODO-TEMPLATE/issues"
51+
"C++ Source" = "https://github.com/NTIA/TODO-TEMPLATE"
52+
"NTIA GitHub" = "https://github.com/NTIA"
53+
"ITS Website" = "https://its.ntia.gov"
54+
55+
[tool.hatch.version]
56+
# TODO-TEMPLATE: Set to this package's `__init__.py`
57+
path = "src/ITS/.../__init__.py"
58+
59+
[tool.hatch.build.targets.wheel]
60+
packages = ["src/ITS"]
61+
ignore-vcs = true
62+
63+
[tool.cibuildwheel]
64+
test-command = "pytest ."
65+
test-requires = "pytest"

src/ITS/PropLibTemplate/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Version X.Y.Z: X.Y is the version of the C++ source,
2+
# and Z is the version of this Python wrapper
3+
__version__ = "1.0.0"
4+
5+
# TODO-TEMPLATE import the functions and objects for the package to expose
6+
# from .proplib_template import (
7+
8+
# )
9+
10+
# TODO-TEMPLATE: Put the name of the module here
11+
__all__ = ["proplib_template"]
+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
This module defines a class for interacting with a compiled shared library using
3+
the ctypes library in Python. It manages loading the library, defining some
4+
expected function prototypes, and parsing exit codes returned by the library functions.
5+
6+
The class `PropLibCDLL` is a thin wrapper for `ctypes.CDLL` which automatically
7+
determines the appropriate shared library file based on the operating system and
8+
provides methods for checking function return codes.
9+
10+
Classes:
11+
--------
12+
- PropLibCDLL: A subclass of `ctypes.CDLL` that manages loading a PropLib shared
13+
library and provides error checking for its functions.
14+
15+
Methods:
16+
--------
17+
- __init__(name):
18+
Initializes the `PropLibCDLL` instance by loading the specified library and
19+
setting up the expected function prototypes.
20+
21+
- get_lib_name(lib_name: str) -> str:
22+
Static method that constructs the full filename of the library based on the
23+
current platform.
24+
25+
- err_check(rtn_code: int) -> None:
26+
Checks the return code from the library's function call and raises a RuntimeError
27+
with the associated error message if the return code indicates an error.
28+
29+
Usage:
30+
------
31+
1. Create an instance of `PropLibCDLL` with the name of the shared library (without
32+
extension).
33+
2. Call functions from the library using the instance.
34+
3. Use `err_check` to handle error codes returned by those functions.
35+
36+
Example:
37+
--------
38+
```python
39+
lib = PropLibCDLL("SomePropLibLibrary-1.0")
40+
return_code = lib.SomeLibraryFunction()
41+
lib.err_check(return_code)
42+
```
43+
"""
44+
import platform
45+
from ctypes import *
46+
from pathlib import Path
47+
48+
class PropLibCDLL(CDLL):
49+
def __init__(self, name):
50+
full_name = self.get_lib_name(name)
51+
super().__init__(full_name)
52+
# Define expected function prototypes
53+
self.GetReturnStatusCharArray.restype = POINTER(c_char_p)
54+
self.GetReturnStatusCharArray.argtypes = (c_int,)
55+
self.FreeReturnStatusCharArray.restype = None
56+
self.FreeReturnStatusCharArray.argtypes = (POINTER(c_char_p),)
57+
58+
@staticmethod
59+
def get_lib_name(lib_name: str) -> str:
60+
"""Get the full filename of the library specified by `lib_name`.
61+
62+
This function appends the correct file extension based on the current platform,
63+
and prepends the full absolute file path. The shared library is expected
64+
to exist in the same directory as this file.
65+
66+
:param lib_name: The library name, with no extension or path, e.g., "P2108-1.0"
67+
:raises NotImplementedError: For platforms other than Windows, Linux, or macOS.
68+
:return: The full filename, including path and extension, of the library.
69+
"""
70+
# Load the compiled library
71+
if platform.uname()[0] == "Windows":
72+
lib_name += ".dll"
73+
elif platform.uname()[0] == "Linux":
74+
lib_name += ".so"
75+
elif platform.uname()[0] == "Darwin":
76+
lib_name += ".dylib"
77+
else:
78+
raise NotImplementedError("Your OS is not yet supported")
79+
# Library should be in the same directory as this file
80+
lib_path = Path(__file__).parent / lib_name
81+
return str(lib_path.resolve())
82+
83+
84+
def err_check(self, rtn_code: int) -> None:
85+
"""Parse the library's return code and raise an error if one occurred.
86+
87+
Returns immediately for `rtn_code == 0`, otherwise retrieves the
88+
status message string from the underlying library and raises a
89+
RuntimeError with the status message.
90+
91+
:param rtn_code: Integer return code from the underlying library.
92+
:raises RuntimeError: For any non-zero inputs.
93+
:return: None
94+
"""
95+
if rtn_code == 0:
96+
return
97+
else:
98+
msg = self.GetReturnStatusCharArray(c_int(rtn_code))
99+
msg_str = cast(msg, c_char_p).value.decode("utf-8")
100+
self.FreeReturnStatusCharArray(msg)
101+
raise RuntimeError(msg_str)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# TODO-TEMPLATE: Rename this file and fix the import namespace
2+
from ITS.PropLibTemplate.proplib_loader import PropLibCDLL
3+
from ctypes import *
4+
5+
# TODO-TEMPLATE: Load the shared library. Example:
6+
# lib = PropLibCDLL("P2108-1.0")
7+
8+
# Define function prototypes
9+
# TODO-TEMPLATE add function prototypes here. Each function should have
10+
# its restype and argtypes defined. Examples:
11+
# lib.AeronauticalStatisticalModel.restype = c_int
12+
# lib.AeronauticalStatisticalModel.argtypes = (
13+
# c_double,
14+
# c_double,
15+
# c_double,
16+
# POINTER(c_double),
17+
# )
18+
19+
# TODO-TEMPLATE: Populate this file with wrapper functions which call the library

tests/test_proplib_template.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# TODO-TEMPLATE: Rename this file
2+
import csv
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
# TODO-TEMPLATE: Import the python wrapper
8+
from ITS import PropLibTemplate
9+
10+
# Test data is expected to exist in parent repository
11+
# (i.e., that this Python wrapper repo is cloned as a submodule of the base repo)
12+
TEST_DATA_DIR = (Path(__file__).parent.parent.parent.parent / "tests") / "data"
13+
ABSTOL__DB = 0.1 # Absolute tolerance, in dB, to ensure outputs match expected value
14+
15+
def read_csv_test_data(filename: str):
16+
with open(TEST_DATA_DIR / filename) as f:
17+
reader = csv.reader(f)
18+
next(reader) # Skip header row
19+
for row in reader:
20+
# yield (inputs, rtn, output)
21+
yield tuple(map(float, row[:-2])), int(row[-2]), float(row[-1])

tox.ini

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[tox]
2+
env_list =
3+
py39
4+
py310
5+
py311
6+
py312
7+
py313
8+
min_version = 4.23.2
9+
skip_missing_interpreters = true
10+
no_package = false
11+
12+
[testenv]
13+
description = Run tests with pytest and generate coverage report
14+
package = wheel
15+
wheel_build_env = .pkg
16+
extras = tests
17+
commands = pytest --cov-report term-missing --no-cov-on-fail --cov {posargs}
18+
19+
[gh] ; GitHub Actions CI with tox-gh
20+
python =
21+
3.9 = py39
22+
3.10 = py310
23+
3.11 = py311
24+
3.12 = py312
25+
3.13 = py313

0 commit comments

Comments
 (0)