Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically generate SPDX3 model classes from spec #738

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c71dacd
wip Generate python model from spec: vocabs
fholger Jul 17, 2023
049f2ec
wip Generate init files for SPDX3 model modules
fholger Jul 18, 2023
3f8d0b5
Add docstrings to generated Vocab enum types
fholger Jul 18, 2023
b9cd909
Generate properties for SPDX3 dataclasses
fholger Jul 25, 2023
b5b6153
Set parent class correctly when generating SPDX dataclasses
fholger Jul 25, 2023
c742cfd
Add docstrings for class properties
fholger Jul 25, 2023
fc43ed7
Collect properties from parent classes, too
fholger Jul 25, 2023
f39930c
Make the string to enum function a static class method of the enum
fholger Jul 25, 2023
0c083ef
Generate constructor with all arguments, including inherited, and pot…
fholger Jul 25, 2023
1eec56b
Remove dead code
fholger Jul 25, 2023
e21ab69
Add abstract constructor if type is not instantiable
fholger Jul 25, 2023
be773b5
Exclude unfinished Extension type
fholger Jul 25, 2023
c4146b9
Use mistletoe for properly reformatting the docstrings
fholger Jul 25, 2023
de4c9e2
Fix ai_package filename
fholger Jul 25, 2023
98eee37
Commit current generator output for review
fholger Jul 25, 2023
3fba351
Properly map core datatypes and xsd types to Python types
fholger Jul 26, 2023
0708303
Properly map lists of DictionaryEntry to python dicts
fholger Jul 26, 2023
9bcbfc9
Exclude classes from being generated if we have a custom type mapping…
fholger Jul 26, 2023
36516c0
Add the model dump from which the classes were generated for reference
fholger Jul 26, 2023
7e0c088
Deal with dashes in camel case names during conversion to snake case …
fholger Jul 26, 2023
1305704
Update generated model files
fholger Jul 26, 2023
20eb9f0
Parse and obey property restrictions in the model dump when generatin…
fholger Jul 27, 2023
fddedfb
Update model dump and generated model classes
fholger Jul 27, 2023
b151fc2
Improve format of generated code by running black and isort on the ou…
fholger Jul 27, 2023
5e68d3e
Prevent redeclaration of existing properties during codegen
fholger Jul 27, 2023
c104c40
Avoid unused imports in generated code
fholger Jul 27, 2023
311ac8b
Exclude generated code from flake8 linting (even though it's mostly f…
fholger Jul 27, 2023
65b79e6
Update generated model files
fholger Jul 27, 2023
49e103e
Refactor code generator; split into multiple files
fholger Jul 27, 2023
8210958
Fix some more errors in generated code
fholger Jul 27, 2023
846759b
Update generated classes
fholger Jul 27, 2023
99d99dd
Update generated model after licensing changes in spec
fholger Jul 28, 2023
072f6e6
Handle owl:Thing as a parent class in spec
fholger Nov 17, 2023
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
134 changes: 134 additions & 0 deletions dev/gen_python_model_from_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# SPDX-FileCopyrightText: 2023 spdx contributors
#
# SPDX-License-Identifier: Apache-2.0

"""
Auto-generates the python model representation from the SPDX3 model spec.

Usage: fetch a fresh copy of the spdx-3-model and the spec-parser, then generate a json dump of the model with
the spec-parser:

python main.py --json-dump ../spdx-3-model/model

Copy the generated `model_dump.json` in `md_generated` next to this file, then run it:

python gen_python_model_from_spec.py

Commit resulting changes.

Note: needs an additional dependency for proper formatting of docstrings:

pip install mistletoe
"""

import json
import os.path
from pathlib import Path

from model_gen.gen_class import GenClassFromSpec
from model_gen.general_templates import FILE_HEADER
from model_gen.utils import (
SPECIAL_TYPE_MAPPINGS,
get_file_path,
get_python_docstring,
get_qualified_name,
namespace_name_to_python,
)
from model_gen.vocab_templates import VOCAB_ENTRY, VOCAB_FILE, VOCAB_STR_TO_VALUE, VOCAB_VALUE_TO_STR

from spdx_tools.spdx.casing_tools import camel_case_to_snake_case

# TODO: use the actual model package path rather than a separate path
output_dir = os.path.join(os.path.dirname(__file__), "../src/spdx_tools/spdx3/new_model")


class GenPythonModelFromSpec:
namespace_imports: str
init_imports: dict[str, dict[str, str]]

def __init__(self):
self.namespace_imports = ""
self.init_imports = {}

def create_namespace_import(self, model: dict):
namespaces = [namespace_name_to_python(namespace["name"]) for namespace in model.values()]
if namespaces:
self.namespace_imports = "from . import " + ", ".join(namespaces)

def handle_class(self, clazz: dict, namespace_name: str, model: dict):
qualified_name = get_qualified_name(clazz["metadata"]["name"], namespace_name)
if qualified_name in SPECIAL_TYPE_MAPPINGS:
# do not generate Python classes for types we are mapping differently
return

clsinfo = GenClassFromSpec(clazz, namespace_name, model, output_dir)
clsinfo.gen_file()

if namespace_name not in self.init_imports:
self.init_imports[namespace_name] = dict()
self.init_imports[namespace_name][clsinfo.filename] = clsinfo.typename

def handle_vocab(self, vocab: dict, namespace_name: str):
typename = vocab["metadata"]["name"]
python_typename = camel_case_to_snake_case(typename)
values_text = "\n".join([VOCAB_ENTRY.format(value=camel_case_to_snake_case(value).upper(),
docstring=get_python_docstring(description, 4)) for
value, description in vocab["entries"].items()])
values_to_str_text = "\n".join([VOCAB_VALUE_TO_STR.format(python_value=camel_case_to_snake_case(value).upper(),
str_value=value, typename=typename) for value in
vocab["entries"]])
str_to_values_text = "\n".join([VOCAB_STR_TO_VALUE.format(python_value=camel_case_to_snake_case(value).upper(),
str_value=value, typename=typename) for value in
vocab["entries"]])
docstring = get_python_docstring(vocab["description"], 4)
file_path = get_file_path(typename, namespace_name, output_dir)
with open(file_path, "w") as output_file:
output_file.write(VOCAB_FILE.format(typename=typename, values=values_text,
values_to_str=values_to_str_text, str_to_values=str_to_values_text,
python_typename=python_typename, docstring=docstring))

if namespace_name not in self.init_imports:
self.init_imports[namespace_name] = dict()
self.init_imports[namespace_name][python_typename] = typename

def handle_namespace(self, namespace: dict, model: dict):
namespace_name = namespace["name"]
namespace_path = os.path.join(output_dir, namespace_name_to_python(namespace_name))
os.makedirs(namespace_path, exist_ok=True)
for clazz in namespace["classes"].values():
self.handle_class(clazz, namespace_name, model)
for vocab in namespace["vocabs"].values():
self.handle_vocab(vocab, namespace_name)

if namespace_name in self.init_imports:
with open(os.path.join(output_dir, namespace_name_to_python(namespace_name), "__init__.py"),
"w") as init_file:
init_file.write(FILE_HEADER)
for module, typename in sorted(self.init_imports[namespace_name].items()):
init_file.write(f"from .{module} import {typename}\n")

def run(self):
os.makedirs(output_dir, exist_ok=True)
Path(os.path.join(output_dir, "__init__.py")).touch()

with open("model_dump.json") as model_file:
model = json.load(model_file)

self.create_namespace_import(model)

for namespace in model.values():
self.handle_namespace(namespace, model)

with open(os.path.join(output_dir, "__init__.py"), "w") as init_file:
init_file.write(FILE_HEADER)
namespace_imports = ", ".join(
[namespace_name_to_python(namespace) for namespace in self.init_imports.keys()])
init_file.write(f"from . import {namespace_imports}\n")
init_file.write("from .core import *\n")

os.system(f'black "{output_dir}"')
os.system(f'isort "{output_dir}"')


if __name__ == "__main__":
GenPythonModelFromSpec().run()
Loading