Skip to content

Instantly share code, notes, and snippets.

@bact
Last active February 23, 2025 09:50
Show Gist options
  • Save bact/ce01c9fb0049a33cfcc9043e8df7c6f9 to your computer and use it in GitHub Desktop.
Save bact/ce01c9fb0049a33cfcc9043e8df7c6f9 to your computer and use it in GitHub Desktop.
Simplify Python binding from shacl2code
# SPDX-FileCopyrightText: 2025-present Arthit Suriyawongkul
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: 0BSD
#
# Generate a simplified interface to classes and functions.
#
# It import all classes and functions from the original module,
# removing the (long IRI-based) prefix from class names,
# and re-export them to the new module.
#
# Intended to use with a Python binding generated from shacl2code.
#
# Usage:
#
# 1. Generate Python binding:
#
# shacl2code generate -i https://spdx.github.io/spdx-spec/v3.0.1/rdf/spdx-model.jsonld python -o spdx3_0_1.py
#
# 2. Simplify Python binding:
#
# python gen_spdx3_model.py spdx3_0_1.py spdx3.py
#
# 3. Ensure spdx3.py is in the same directory as spdx3_0_1.py
# 4. Import `spdx3` instead of `spdx3_0_1`
#
# Example:
# Instead of:
# from . import spdx3_0_1.https_spdx_org_rdf_3_0_1_terms_Core_Element
# Use:
# from . import spdx3.Core_Element
#
# Note:
# Class prefixes prevent naming conflicts that can arise when SHACL uses
# classes from different namespaces. This utility removes the prefix for
# specific use cases where it's unnecessary.
import inspect
import importlib.util
import argparse
import os
_SPDX_3_0_1_CLASS_PREFIX = "https_spdx_org_rdf_3_0_1_terms_"
def import_and_rename_classes(module, prefix):
classes = {}
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and name.startswith(prefix):
new_class_name = name[len(prefix) :] # Remove the prefix
classes[new_class_name] = obj
return classes
def import_functions(module):
functions = {}
for name, obj in inspect.getmembers(module):
if inspect.isfunction(obj):
functions[name] = obj
return functions
def load_module_from_file(module_name, file_path):
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def generate_module_file(input_path, prefix, output_path):
input_file_name = os.path.basename(input_path)
output_file_name = os.path.basename(output_path)
input_module_name = os.path.splitext(input_file_name)[0]
output_module_name = os.path.splitext(output_file_name)[0]
module = load_module_from_file(input_module_name, input_path)
classes = import_and_rename_classes(module, prefix)
functions = import_functions(module)
with open(output_path, "w") as f:
f.write(
f"""
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: 0BSD
#
# Auto-generated module. DO NOT MANUALLY MODIFY.
#
# This module provides a simplified interface to classes and functions
# originally defined in the `{module.__name__}` module.
#
# It re-exports all classes and functions from `{module.__name__}`,
# removing the following prefix from class names:
# '{prefix}'
#
# Usage:
# 1. Ensure this file ({output_file_name}) is in the same directory as {input_file_name}.
# 2. Import this module (`{output_module_name}`) instead of `{module.__name__}`.
#
# Example:
# Instead of:
# from . import {module.__name__}.{prefix}Core_Element
# Use:
# from . import {output_module_name}.Core_Element
"""
)
f.write(f"\nfrom . import {module.__name__} as _reexport\n\n")
# Write classes
for new_class_name, original_class in classes.items():
f.write(f"{new_class_name} = _reexport.{original_class.__name__}\n")
f.write("\n")
# Write functions
for func_name, func in functions.items():
f.write(f"{func_name} = _reexport.{func.__name__}\n")
f.write("\n")
# Write __all__ variable
all_items = list(classes.keys()) + list(functions.keys())
f.write("__all__ = [\n")
for item in all_items:
f.write(f" '{item}',\n")
f.write("]\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate a module that re-exports classes and functions from a specified module."
)
parser.add_argument(
"input_path",
type=str,
help="The path to the Python module input file",
)
parser.add_argument(
"output_path",
type=str,
help="The path to the Python module output file",
)
parser.add_argument(
"--class_prefix",
type=str,
default=_SPDX_3_0_1_CLASS_PREFIX,
help="The prefix to be removed from class names. default: %(default)s)",
)
args = parser.parse_args()
generate_module_file(args.input_path, args.class_prefix, args.output_path)
print(f"Module file generated at {args.output_path}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment