Thanks for the help @aothms !
I see... so there's not any straightforward solution. I wonder why these two lists aren't an Enum in IFC, although that doesn't seem like a quick and easy thing to change.
I got into this one because I was thinking of the high level API as a tool which could validate arguments at runtime up to some useful degree of correctness (as hinted in #3774). In addition to ifcopenshell.validate
for EXPRESS rules, which although being ultra powerful, could make it for a slower workflow to fix dumb mistakes. For instance, I'm thinking something that catches "Axes" identifier, instead of "Axis", when creating a representation subcontext, that kind of thing. Maybe it looks like a very minor issue, but I believe a good API design should be explicit on what it covers, issuing warnings or raising errors when any edge case falls outside of what the code was conceived for. That, in turn, makes the little technical debt more visible and thus easier to be addressed with time.
A variation of the abstract syntax tree approach (Python case), returning only the values of interest, could be as in the code below. But of course, 1) this is not handy at all to perform runtime checks and 2) the visit method is curated for exactly this specific EXPRESS function... which feels very volatile.
import ast
import ifcopenshell.express.rules as rules
import importlib
import inspect
from dataclasses import dataclass, field
**@dataclass**(slots=True)
class ShapeRepresentationTypesVisitor(ast.NodeVisitor):
version: str = 'IFC4'
shape_representation_types: list[str] = field(init=False, default_factory=list)
def __post_init__(self) -> None:
express_ifc = importlib.import_module(f'{rules.__name__}.{self.version}')
source: str = inspect.getsource(express_ifc.IfcShapeRepresentationTypes)
tree: ast.Module = ast.parse(source)
self.visit(tree)
**@classmethod**
def get_enum(cls, *args, **kwargs) -> list[str]:
return cls(*args, **kwargs).shape_representation_types
def visit_If(self, node: ast.If) -> None:
for child in ast.iter_child_nodes(node):
if not isinstance(child, ast.Compare):
continue
for constant in child.comparators:
if not isinstance(constant, ast.Constant):
continue
value = constant.value
if not isinstance(value, str):
continue
self.shape_representation_types.append(value)
self.generic_visit(node)
And then the representation types could be obtained as (for any required IFC version):
ShapeRepresentationTypesVisitor.get_enum(version='IFC2X3'))
So in summary, I guess the best option for what I'm thinking is to just use this code offline and hardcode the representation types for each schema version.