diff --git a/honeybee/cli/validate.py b/honeybee/cli/validate.py index ac21b473..35128a5f 100644 --- a/honeybee/cli/validate.py +++ b/honeybee/cli/validate.py @@ -1,7 +1,7 @@ """honeybee validation commands.""" import sys import logging -import json +import json as py_json import click from honeybee.model import Model @@ -16,7 +16,7 @@ def validate(): @validate.command('model') -@click.argument('model-json', type=click.Path( +@click.argument('model-file', type=click.Path( exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @click.option( '--check-all/--room-overlaps', ' /-ro', help='Flag to note whether the output ' @@ -37,8 +37,8 @@ def validate(): '--output-file', '-f', help='Optional file to output the full report ' 'of the validation. By default it will be printed out to stdout', type=click.File('w'), default='-') -def validate_model(model_json, check_all, plain_text, output_file): - """Validate all properties of a Model file against the Honeybee schema. +def validate_model_cli(model_file, check_all, plain_text, output_file): + """Validate all properties of a Model file against Honeybee schema. This includes checking basic compliance with the 5 rules of honeybee geometry as well as checks for all extension attributes. The 5 rules of honeybee geometry @@ -58,53 +58,12 @@ def validate_model(model_json, check_all, plain_text, output_file): \b Args: - model_json: Full path to a Model JSON file. + model_file: Full path to a Model JSON file. """ try: - if plain_text: - # re-serialize the Model to make sure no errors are found - c_ver = folders.honeybee_core_version_str - s_ver = folders.honeybee_schema_version_str - click.echo( - 'Validating Model using honeybee-core=={} and ' - 'honeybee-schema=={}'.format(c_ver, s_ver) - ) - parsed_model = Model.from_hbjson(model_json) - click.echo('Re-serialization passed.') - # perform several other checks for geometry rules and others - if check_all: - report = parsed_model.check_all(raise_exception=False, detailed=False) - else: - report = parsed_model.check_room_volume_collisions(raise_exception=False) - click.echo('Model checks completed.') - # check the report and write the summary of errors - if report == '': - output_file.write('Congratulations! Your Model is valid!') - else: - error_msg = '\nYour Model is invalid for the following reasons:' - output_file.write('\n'.join([error_msg, report])) - else: - out_dict = { - 'type': 'ValidationReport', - 'app_name': 'Honeybee', - 'app_version': folders.honeybee_core_version_str, - 'schema_version': folders.honeybee_schema_version_str - } - try: - parsed_model = Model.from_hbjson(model_json) - out_dict['fatal_error'] = '' - if check_all: - errors = parsed_model.check_all(raise_exception=False, detailed=True) - else: - errors = parsed_model.check_room_volume_collisions( - raise_exception=False, detailed=True) - out_dict['errors'] = errors - out_dict['valid'] = True if len(out_dict['errors']) == 0 else False - except Exception as e: - out_dict['fatal_error'] = str(e) - out_dict['errors'] = [] - out_dict['valid'] = False - output_file.write(json.dumps(out_dict, indent=4)) + json = not plain_text + room_overlaps = not check_all + validate_model(model_file, room_overlaps, json, output_file) except Exception as e: _logger.exception('Model validation failed.\n{}'.format(e)) sys.exit(1) @@ -112,14 +71,81 @@ def validate_model(model_json, check_all, plain_text, output_file): sys.exit(0) +def validate_model(model_file, room_overlaps=False, json=False, output_file=None, + check_all=True, plain_text=True): + """Validate all properties of a Model file against the Honeybee schema. + + This includes checking basic compliance with the 5 rules of honeybee geometry + as well as checks for all extension attributes. + + Args: + model_file: Full path to a Honeybee Model file. + room_overlaps: Boolean to note whether the output validation report + should only validate the Room collisions (True) or all possible + issues with the model should be checked (False). (Default: False). + json: Boolean to note whether the output validation report should be + formatted as a JSON object instead of plain text. + """ + if not json: + # re-serialize the Model to make sure no errors are found + c_ver = folders.honeybee_core_version_str + s_ver = folders.honeybee_schema_version_str + ver_msg = 'Validating Model using honeybee-core=={} and ' \ + 'honeybee-schema=={}'.format(c_ver, s_ver) + print(ver_msg) + parsed_model = Model.from_file(model_file) + print('Re-serialization passed.') + # perform several other checks for geometry rules and others + if not room_overlaps: + report = parsed_model.check_all(raise_exception=False, detailed=False) + else: + report = parsed_model.check_room_volume_collisions(raise_exception=False) + print('Model checks completed.') + # check the report and write the summary of errors + if report == '': + full_msg = ver_msg + '\nCongratulations! Your Model is valid!' + else: + full_msg = ver_msg + \ + '\nYour Model is invalid for the following reasons:\n' + report + if output_file is None: + return full_msg + else: + output_file.write(full_msg) + else: + out_dict = { + 'type': 'ValidationReport', + 'app_name': 'Honeybee', + 'app_version': folders.honeybee_core_version_str, + 'schema_version': folders.honeybee_schema_version_str + } + try: + parsed_model = Model.from_file(model_file) + out_dict['fatal_error'] = '' + if not room_overlaps: + errors = parsed_model.check_all(raise_exception=False, detailed=True) + else: + errors = parsed_model.check_room_volume_collisions( + raise_exception=False, detailed=True) + out_dict['errors'] = errors + out_dict['valid'] = True if len(out_dict['errors']) == 0 else False + except Exception as e: + out_dict['fatal_error'] = str(e) + out_dict['errors'] = [] + out_dict['valid'] = False + if output_file is None: + return py_json.dumps(out_dict, indent=4) + else: + output_file.write(py_json.dumps(out_dict, indent=4)) + + @validate.command('room-volumes') -@click.argument('model-json', type=click.Path( +@click.argument('model-file', type=click.Path( exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @click.option( '--output-file', '-f', help='Optional file to output the JSON strings of ' 'ladybug_geometry LineSegment3Ds that represent naked and non-manifold edges. ' 'By default it will be printed out to stdout', type=click.File('w'), default='-') -def validate_room_volumes(model_json, output_file): +def validate_room_volumes(model_file, output_file): """Get a list of all naked and non-manifold edges preventing closed room volumes. This is helpful for visually identifying issues in geometry that are preventing @@ -127,11 +153,11 @@ def validate_room_volumes(model_json, output_file): \b Args: - model_json: Full path to a Model JSON file. + model_file: Full path to a Honeybee Model file. """ try: # re-serialize the Model and collect all naked and non-manifold edges - parsed_model = Model.from_hbjson(model_json) + parsed_model = Model.from_file(model_file) problem_edges = [] for room in parsed_model.rooms: if not room.geometry.is_solid: @@ -139,7 +165,7 @@ def validate_room_volumes(model_json, output_file): problem_edges.extend(room.geometry.non_manifold_edges) # write the new model out to the file or stdout prob_array = [lin.to_dict() for lin in problem_edges] - output_file.write(json.dumps(prob_array)) + output_file.write(py_json.dumps(prob_array)) except Exception as e: _logger.exception('Model room volume validation failed.\n{}'.format(e)) sys.exit(1) diff --git a/tests/cli_validate_test.py b/tests/cli_validate_test.py index 2c67d771..4f0cd141 100644 --- a/tests/cli_validate_test.py +++ b/tests/cli_validate_test.py @@ -4,7 +4,7 @@ from click.testing import CliRunner from honeybee.cli import viz, config -from honeybee.cli.validate import validate_model +from honeybee.cli.validate import validate_model_cli def test_viz(): @@ -28,10 +28,10 @@ def test_validate_model(): incorrect_input_model = './tests/json/bad_geometry_model.hbjson' if (sys.version_info >= (3, 7)): runner = CliRunner() - result = runner.invoke(validate_model, [input_model]) + result = runner.invoke(validate_model_cli, [input_model]) assert result.exit_code == 0 runner = CliRunner() - result = runner.invoke(validate_model, [incorrect_input_model]) + result = runner.invoke(validate_model_cli, [incorrect_input_model]) outp = result.output assert 'Your Model is invalid for the following reasons' in outp assert 'is not coplanar or fully bounded by its parent Face' in outp @@ -42,13 +42,13 @@ def test_validate_model_json(): incorrect_input_model = './tests/json/bad_geometry_model.hbjson' if (sys.version_info >= (3, 7)): runner = CliRunner() - result = runner.invoke(validate_model, [input_model, '--json']) + result = runner.invoke(validate_model_cli, [input_model, '--json']) assert result.exit_code == 0 outp = result.output valid_report = json.loads(outp) assert valid_report['valid'] runner = CliRunner() - result = runner.invoke(validate_model, [incorrect_input_model, '--json']) + result = runner.invoke(validate_model_cli, [incorrect_input_model, '--json']) outp = result.output valid_report = json.loads(outp) assert not valid_report['valid'] @@ -59,7 +59,7 @@ def test_validate_mismatched_adjacency(): incorrect_input_model = './tests/json/mismatched_area_adj.hbjson' if (sys.version_info >= (3, 7)): runner = CliRunner() - result = runner.invoke(validate_model, [incorrect_input_model, '--json']) + result = runner.invoke(validate_model_cli, [incorrect_input_model, '--json']) outp = result.output valid_report = json.loads(outp) assert not valid_report['valid'] @@ -71,7 +71,7 @@ def test_colliding_room_volumes(): incorrect_input_model = './tests/json/colliding_room_volumes.hbjson' if (sys.version_info >= (3, 7)): runner = CliRunner() - result = runner.invoke(validate_model, [incorrect_input_model, '--json']) + result = runner.invoke(validate_model_cli, [incorrect_input_model, '--json']) outp = result.output valid_report = json.loads(outp) assert not valid_report['valid']