ty: detect unresolved references

This commit is contained in:
Andrej730
2026-07-18 20:35:10 +05:00
parent f744753726
commit b35f99e63f
99 changed files with 401 additions and 84 deletions
@@ -159,8 +159,10 @@ def _add_segment_to_curve(
else:
assert False
end_point = ...
for mapped_segment in mapped_segments:
if mapped_segment:
end_point = _add_curve_segment_to_composite_curve(file, layout_segment, mapped_segment, curve)
assert end_point is not ...
return end_point
@@ -308,5 +308,7 @@ def _get_segment_start_point_label(prev_segment: entity_instance, segment: entit
label = _cant_callback(prev_segment, segment)
else:
label = _cant_label(prev_segment, segment)
else:
assert False, s.DesignParameters
return label
@@ -46,6 +46,7 @@ def _create_layout(file: ifcopenshell.file, alignment: entity_instance, points:
ifcopenshell.api.nest.assign_object(file, related_objects=alignment_layouts, relating_object=alignment)
start_dist_along = 0.0
gradient = None
for p1, p2 in zip(points, points[1:]):
x1, y1, z1 = p1.Coordinates
x2, y2, z2 = p2.Coordinates
@@ -100,6 +101,7 @@ def _create_layout(file: ifcopenshell.file, alignment: entity_instance, points:
ifcopenshell.api.nest.assign_object(file, related_objects=[hsegment], relating_object=alignment_layouts[0])
if include_vertical:
assert gradient is not None
vsegment = file.createIfcAlignmentSegment(
ifcopenshell.guid.new(),
DesignParameters=file.createIfcAlignmentVerticalSegment(
@@ -57,6 +57,7 @@ def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
:param filepath: path the to CSV file
:return: IfcAlignment
"""
alignment = None
with open(filepath, newline="") as csvfile:
reader = csv.reader(csvfile)
row_count = 0
@@ -89,9 +90,14 @@ def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
)
else:
# add all subsequent vertical alignments
assert alignment is not None
vertical_layout = ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(
file, vertical_layout, coordinates, radii
)
if row_count == 0:
raise ValueError(f"CSV file '{filepath}' is empty; expected at least one row for the horizontal alignment.")
assert alignment is not None
return alignment
@@ -182,6 +182,8 @@ class Usecase:
if not reference:
migrator = ifcopenshell.util.schema.Migrator()
old_referenced_source = ...
existing_classification = None
if self.settings["is_lightweight"]:
old_referenced_source = self.settings["reference"].ReferencedSource
self.settings["reference"].ReferencedSource = None
@@ -194,6 +196,7 @@ class Usecase:
reference = migrator.migrate(self.settings["reference"], self.file)
if self.settings["is_lightweight"]:
assert old_referenced_source is not ...
reference.ReferencedSource = self.settings["classification"]
self.settings["reference"].ReferencedSource = old_referenced_source
elif existing_classification:
@@ -88,6 +88,8 @@ def bearing2dd(bearing: str) -> float:
elif cY == "S" and cX == "W":
angle = 270.0
sign = -1.0
else:
assert False, (cY, cX)
try:
dms = ifcopenshell.util.geolocation.dms2dd(d, m, s, ms)
@@ -116,6 +116,8 @@ def add_feature(
return ifcopenshell.api.aggregate.assign_object(file, [feature], element)
rels = feature.AdheresToElement
ifc_class = "IfcRelAdheresToElement"
else:
assert False, feature
if rels:
if rels[0][4] == element:
@@ -54,6 +54,8 @@ def remove_feature(file: ifcopenshell.file, feature: ifcopenshell.entity_instanc
rels = []
else:
rels = feature.ProjectsElements
else:
assert False, feature
for rel in rels:
history = rel.OwnerHistory
file.remove(rel)
@@ -436,6 +436,7 @@ class Usecase:
def create_curve_bounded_planes(self, is_2d: bool = False) -> list[ifcopenshell.entity_instance]:
items = []
points = None
if self.file.schema != "IFC2X3":
points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=False)
for polygon in self.settings["geometry"].polygons:
@@ -443,6 +444,7 @@ class Usecase:
if self.file.schema == "IFC2X3":
curve = self.create_curve_from_polygon_ifc2x3(polygon, is_2d=False)
else:
assert points is not None
curve = self.create_curve_from_polygon(points, polygon, is_2d=False)
items.append(self.file.createIfcCurveBoundedPlane(BasisSurface=plane, OuterBoundary=curve))
return items
@@ -457,12 +459,14 @@ class Usecase:
def create_annotation_fill_areas(self, is_2d: bool = False) -> list[ifcopenshell.entity_instance]:
items = []
points = None
if self.file.schema != "IFC2X3":
points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=is_2d)
for polygon in self.settings["geometry"].polygons:
if self.file.schema == "IFC2X3":
curve = self.create_curve_from_polygon_ifc2x3(polygon, is_2d=is_2d)
else:
assert points is not None
curve = self.create_curve_from_polygon(points, polygon, is_2d=is_2d)
items.append(self.file.createIfcAnnotationFillArea(OuterBoundary=curve))
return items
@@ -813,17 +817,20 @@ class Usecase:
def create_triangulated_face_set(self) -> ifcopenshell.entity_instance:
ifc_raw_items = [None] * self.settings["total_items"]
ifc_raw_uv_items = None
if self.settings["should_generate_uvs"]:
ifc_raw_uv_items = [None] * self.settings["total_items"]
for i, value in enumerate(ifc_raw_items):
ifc_raw_items[i] = []
if self.settings["should_generate_uvs"]:
assert ifc_raw_uv_items is not None
ifc_raw_uv_items[i] = []
for polygon in self.settings["geometry"].polygons:
ifc_raw_items[polygon.material_index % self.settings["total_items"]].append(
[v + 1 for v in polygon.vertices]
)
if self.settings["should_generate_uvs"]:
assert ifc_raw_uv_items is not None
ifc_raw_uv_items[polygon.material_index % self.settings["total_items"]].append(
[uv + 1 for uv in polygon.loop_indices]
)
@@ -831,6 +838,7 @@ class Usecase:
coordinates = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices)
if self.settings["should_generate_uvs"]:
assert ifc_raw_uv_items is not None
# Blender supports multiple UV layers. We don't. Too bad.
tex_coords = self.file.createIfcTextureVertexList(
[tuple(x.uv) for x in self.settings["geometry"].uv_layers[0].data]
@@ -50,6 +50,12 @@ def disconnect_path(
for r in relating_element.ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == related_element
]
else:
raise ValueError(
"Either provide `element` and `connection_type`, or provide `relating_element` and `related_element`. "
f"Got: element={element}, connection_type={connection_type}, "
f"relating_element={relating_element}, related_element={related_element}."
)
for connection in set(connections):
history = connection.OwnerHistory
@@ -52,6 +52,7 @@ def edit_true_north(file: ifcopenshell.file, true_north: Optional[Union[tuple[fl
# This unsets true north
ifcopenshell.api.georeference.edit_true_north(model, true_north=None)
"""
x, y = None, None
if isinstance(true_north, (float, int)):
x, y = ifcopenshell.util.geolocation.angle2yaxis(true_north)
elif true_north is not None:
@@ -73,4 +74,5 @@ def edit_true_north(file: ifcopenshell.file, true_north: Optional[Union[tuple[fl
context.TrueNorth = file.create_entity("IfcDirection")
else:
context.TrueNorth = file.create_entity("IfcDirection")
assert x is not None and y is not None
context.TrueNorth.DirectionRatios = (x, y)
@@ -90,6 +90,8 @@ def edit_wcs(
point,
file.createIfcDirection((xaxis_x, xaxis_y)),
)
else:
assert False, context
context.WorldCoordinateSystem = placement
if file.get_total_inverses(old_wcs) == 0:
ifcopenshell.util.element.remove_deep2(file, old_wcs)
@@ -61,6 +61,8 @@ def add_structural_boundary_condition(
boundary_class = "IfcBoundaryEdgeCondition"
elif related_connection.is_a("IfcStructuralSurfaceConnection"):
boundary_class = "IfcBoundaryFaceCondition"
else:
assert False, related_connection
condition = file.create_entity(boundary_class, Name=name)
connection.AppliedCondition = condition
@@ -126,6 +126,7 @@ class Usecase:
use_style_assignment = self.file.schema == "IFC2X3" or self.settings["should_use_presentation_style_assignment"]
replace_previous_same_type_style = self.settings["replace_previous_same_type_style"]
style: ifcopenshell.entity_instance | None = None
for element in self.file.traverse(self.settings["shape_representation"]):
if not element.is_a("IfcShapeModel"):
continue
@@ -137,6 +138,7 @@ class Usecase:
if self.settings["styles"]:
# If there are more items than styles, fallback to using the last style
style = self.settings["styles"].pop(0)
assert style is not None
name = style.Name
current_style_type = style.is_a()
@@ -51,7 +51,8 @@ def assign_system(
# This duct is part of the system
ifcopenshell.api.system.assign_system(model, products=[duct], system=system)
"""
if not all(ifcopenshell.util.system.is_assignable(failed_product := product, system) for product in products):
raise TypeError(f"You cannot assign an {failed_product.is_a()} to an {system.is_a()}")
for product in products:
if not ifcopenshell.util.system.is_assignable(product, system):
raise TypeError(f"You cannot assign an {product.is_a()} to an {system.is_a()}")
return ifcopenshell.api.group.assign_group(file, products=products, group=system)
@@ -144,6 +144,8 @@ class Usecase:
elif unit_type == "volume":
dimensional_exponents = self.file.createIfcDimensionalExponents(3, 0, 0, 0, 0, 0, 0)
name_prefix = "cubic"
else:
assert False, unit_type
si_unit = self.file.createIfcSIUnit(
None,
@@ -159,6 +161,8 @@ class Usecase:
name = "{}mile".format(name_prefix + " " if name_prefix else "")
elif data["raw"] == "THOU":
name = "{}thou".format(name_prefix + " " if name_prefix else "")
else:
assert False, data
value_component = self.file.create_entity(
"IfcReal", **{"wrappedValue": ifcopenshell.util.unit.si_conversions[name]}
)
@@ -296,6 +296,7 @@ def main(
else:
num_passes = 0
g2 = None
for iteration in range(num_passes + 1):
# initialize empty group, note that in the current approach only one
@@ -316,6 +317,7 @@ def main(
plt.fill(numpy.array(x.boundary).T[0], numpy.array(x.boundary).T[1])
"""
semantics, pairs = None, None
if iteration != num_passes:
pairs = svgfill_context.get_face_pairs()
semantics = [None] * (max(pairs) + 1)
@@ -377,6 +379,7 @@ def main(
if inside_elements:
elements = None
if iteration != num_passes:
assert semantics is not None
semantics[pi] = (inside_elements[0], -1)
else:
elements = tree.select_ray(pythonize(a), pythonize(b - a))
@@ -409,6 +412,7 @@ def main(
svg_fill = "rgb(%s)" % ", ".join(str(f * 255.0) for f in clr[0:3])
if iteration != num_passes:
assert semantics is not None
semantics[pi] = elements[0]
else:
svg_fill = "none"
@@ -418,6 +422,8 @@ def main(
if iteration != num_passes:
to_remove = []
assert pairs is not None
assert semantics is not None
for he_idx in range(0, len(pairs), 2):
# @todo instead of ray_distance, better do (x.point - y.point).dot(x.normal)
# to see if they're coplanar, because ray-distance will be different in case
@@ -445,6 +451,7 @@ def main(
# Swap the XML nodes from the files
# Remove the original hidden line node we still have in the serializer output
assert g2 is not None
g1.removeChild(projection)
g2.setAttribute("class", "projection")
# Find the children of the projection node parent
@@ -192,7 +192,7 @@ class entity_instance:
return
@property
def file(self):
def file(self) -> "ifcopenshell.file":
# ugh circular imports, name collisions
from . import file
@@ -734,6 +734,7 @@ class file:
# Don't store these attributes as transactions
# as the creation it self is already stored with
# it's arguments
transaction = None
if attrs:
transaction = self.transaction
self.transaction = None
@@ -849,11 +850,13 @@ class file:
:returns: An ifcopenshell.entity_instance
"""
max_id = None
if self.transaction:
max_id = self.wrapped_data.getMaxId()
inst.wrapped_data.this.disown()
result = entity_instance(self.wrapped_data.add(inst.wrapped_data, -1 if _id is None else _id), self)
if self.transaction:
assert max_id is not None
added_elements = [e for e in self.traverse(result) if e.id() > max_id]
[self.transaction.store_create(e) for e in reversed(added_elements)]
return result
@@ -326,10 +326,11 @@ class iterator(ifcopenshell_wrapper.Iterator):
if include_or_exclude_type == {"entity_instance"}:
include_or_exclude = cast(set[entity_instance], include_or_exclude)
if not all((last_inst := inst).is_a("IfcProduct") for inst in include_or_exclude):
raise ValueError(
f"include and exclude need to be an aggregate of IfcProduct. Violating element: '{last_inst}'."
)
for inst in include_or_exclude:
if not inst.is_a("IfcProduct"):
raise ValueError(
f"include and exclude need to be an aggregate of IfcProduct. Violating element: '{inst}'."
)
initializer = ifcopenshell_wrapper.construct_iterator_with_include_exclude_id
@@ -1070,7 +1070,10 @@ class file:
"""
...
def getMaxId(self): ...
def getMaxId(self) -> int:
"""Get the highest instance id currently in use in the file."""
...
def get_total_inverses_by_id(self, instance_id: int) -> int: ...
def getUnit(self, unit_type): ...
def get_inverse(self, e: entity_instance) -> tuple[entity_instance, ...]: ...
@@ -112,6 +112,8 @@ def sum_child_root_elements(root_element: ifcopenshell.entity_instance, category
values = new_child_root_element.CostValues
elif root_element.is_a("IfcConstructionResource"):
values = child_root_element.BaseCosts
else:
assert False, root_element
for child_cost_value in values or []:
if category_filter and child_cost_value.Category != category_filter:
continue
@@ -329,6 +329,8 @@ def get_quantity(
data["properties"] = get_quantities(quantity.HasQuantities, verbose=verbose)
del data["HasQuantities"]
result = data
else:
assert False, quantity
if verbose:
result = {"id": quantity.id(), "class": quantity.is_a(), "value": result}
return result
@@ -385,6 +387,7 @@ def get_property(
if prop.Name != name:
continue
is_single_value = False # For now we pass value type only for single values.
result_type = None
if prop.is_a("IfcPropertySingleValue"):
# 2 IfcPropertySingleValue.NominalValue
result = v.wrappedValue if (v := prop[2]) else None
@@ -407,6 +410,8 @@ def get_property(
data["properties"] = get_properties(prop.HasProperties, verbose=verbose)
del data["HasProperties"]
result = data
else:
assert False, prop
if verbose:
result = {"id": prop.id(), "class": prop.is_a(), "value": result}
if is_single_value:
@@ -260,6 +260,8 @@ def get_helmert_transformation_parameters(ifc_file: ifcopenshell.file) -> Option
xaa = 1.0
xao = 0.0
scale = factor_x = factor_y = factor_z = 1
else:
assert False, conversion
if not xaa and not xao:
xaa = 1.0
@@ -18,18 +18,16 @@
from __future__ import annotations
try:
from lark import Lark, Transformer
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
LARK_AVAILABLE = True
except ImportError:
LARK_AVAILABLE = False
import importlib.util
import re
from typing import Union
LARK_AVAILABLE = importlib.util.find_spec("lark") is not None
if LARK_AVAILABLE:
from lark import Lark, Transformer
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
mvd_grammar = r"""
start: entry+
@@ -92,9 +90,9 @@ if LARK_AVAILABLE:
self.store_text_attribute(args, "options")
def dynamic_option(self, args):
original_keyword = str(args[0])
key = original_keyword.lower()
try:
original_keyword = str(args[0])
key = original_keyword.lower()
raw_text = args[1].children[0].value
parsed_value = parse_semicolon_separated_kv(raw_text)
self._dynamic[key] = (parsed_value, original_keyword)
@@ -89,6 +89,9 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
x = np.array((1, 0, 0))
o = placement.Location.Coordinates
else:
assert False, placement
return a2p(o, z, x)
@@ -584,6 +584,7 @@ class Migrator:
# NOTE: `attribute` is an attribute in new file schema
# print("Migrating attribute", element, new_element, attribute.name())
old_file = element.wrapped_data.file
value = ...
if hasattr(element, attribute.name()):
value = getattr(element, attribute.name())
# print("Attribute names matched", value)
@@ -622,9 +623,7 @@ class Migrator:
except: # We tried our best
return
try:
value
except UnboundLocalError:
if value is ...:
print(
f"Couldn't match attribute {attribute.name()} by name to migrate from {element} "
f"to {new_element} and there is no special mapping to handle migration "
@@ -1279,6 +1279,8 @@ class FacetTransformer(lark.Transformer):
result = bool(value.match(element_value)) if element_value is not None else False
elif value in (None, True, False):
result = element_value is value
else:
assert False, value
if comparison.startswith("!"):
return not result
@@ -210,6 +210,8 @@ def np_rotation_matrix(
matrix = np.array([[cos_theta, 0, sin_theta], [0, 1, 0], [-sin_theta, 0, cos_theta]])
elif axis == "Z":
matrix = np.array([[cos_theta, -sin_theta, 0], [sin_theta, cos_theta, 0], [0, 0, 1]])
else:
assert False, axis
else:
# Assume axis is a vector.
axis = axis / np.linalg.norm(axis)
@@ -320,6 +320,7 @@ def log_internal_cpp_errors(
if log_content is None:
log_content = ifcopenshell.get_log()
lines = None
msgs = list(map(json.loads, filter(None, log_content.split("\n"))))
chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs]
instance_messages = [for_instance_re.findall(m["message"]) for m in msgs]
@@ -356,6 +357,7 @@ def log_internal_cpp_errors(
except:
inst = None
else:
assert lines is not None
inst = next(
(
l.decode("ascii", errors="ignore").strip()
@@ -691,14 +693,16 @@ def validate_ifc_header(
if not value:
log_error(header_entity, name, index, AGGREGATE_TYPE, "EMPTY LIST")
return
if not all(isinstance(last_value := v, str) for v in value):
log_error(
header_entity,
name,
index,
AGGREGATE_TYPE,
f"LIST with {type(last_value).__name__} (value: {last_value})",
)
for v in value:
if not isinstance(v, str):
log_error(
header_entity,
name,
index,
AGGREGATE_TYPE,
f"LIST with {type(v).__name__} (value: {v})",
)
break
return
if not isinstance(value, str):
+4 -2
View File
@@ -26,6 +26,8 @@ def test_file_gc(args):
inst = f.createIfcPerson()
elif api in (1, 2):
inst = f.createIfcSite()
else:
assert False, api
r = weakref.ref(f)
@@ -68,9 +70,9 @@ def test_file_gc(args):
assert r()
if not file_first:
del f
del f # ty: ignore[possibly-unresolved-reference]
else:
del inst
del inst # ty: ignore[possibly-unresolved-reference]
# With both deleted we should have no longer access to the file.
assert r() is None